diff --git a/src/pages/process-element/process-element.ts b/src/pages/process-element/process-element.ts index 2a6ae9d..52c89b5 100755 --- a/src/pages/process-element/process-element.ts +++ b/src/pages/process-element/process-element.ts @@ -1,50 +1,50 @@ -import { interpolate } from '../../utils/html.utils'; -import Services from '../../services/service'; -import { Process } from '../../pkg/sdk_client.js'; -import { getCorrectDOM } from '~/utils/document.utils'; - -let currentPageStyle: HTMLStyleElement | null = null; - -export async function initProcessElement(id: string, zone: string) { - const processes = await getProcesses(); - const container = getCorrectDOM('process-4nk-component'); - // const currentProcess = processes.find((process) => process[0] === id)[1]; - // const currentProcess = {title: 'Hello', html: '', css: ''}; - // await loadPage({ processTitle: currentProcess.title, inputValue: 'Hello World !' }); - // const wrapper = document.querySelector('.process-container'); - // if (wrapper) { - // wrapper.innerHTML = interpolate(currentProcess.html, { processTitle: currentProcess.title, inputValue: 'Hello World !' }); - // injectCss(currentProcess.css); - // } -} - -async function loadPage(data?: any) { - const content = document.getElementById('containerId'); - if (content && data) { - if (data) { - content.innerHTML = interpolate(content.innerHTML, data); - } - } -} - -function injectCss(cssContent: string) { - removeCss(); // Ensure that the previous CSS is removed - - currentPageStyle = document.createElement('style'); - currentPageStyle.type = 'text/css'; - currentPageStyle.appendChild(document.createTextNode(cssContent)); - document.head.appendChild(currentPageStyle); -} - -function removeCss() { - if (currentPageStyle) { - document.head.removeChild(currentPageStyle); - currentPageStyle = null; - } -} - -async function getProcesses(): Promise> { - const service = await Services.getInstance(); - const processes = await service.getProcesses(); - return processes; -} +import { interpolate } from '../../utils/html.utils'; +import Services from '../../services/service'; +import { Process } from '../../pkg/sdk_client.js'; +import { getCorrectDOM } from '~/utils/document.utils'; + +let currentPageStyle: HTMLStyleElement | null = null; + +export async function initProcessElement(id: string, zone: string) { + const processes = await getProcesses(); + const container = getCorrectDOM('process-4nk-component'); + // const currentProcess = processes.find((process) => process[0] === id)[1]; + // const currentProcess = {title: 'Hello', html: '', css: ''}; + // await loadPage({ processTitle: currentProcess.title, inputValue: 'Hello World !' }); + // const wrapper = document.querySelector('.process-container'); + // if (wrapper) { + // wrapper.innerHTML = interpolate(currentProcess.html, { processTitle: currentProcess.title, inputValue: 'Hello World !' }); + // injectCss(currentProcess.css); + // } +} + +async function loadPage(data?: any) { + const content = document.getElementById('containerId'); + if (content && data) { + if (data) { + content.innerHTML = interpolate(content.innerHTML, data); + } + } +} + +function injectCss(cssContent: string) { + removeCss(); // Ensure that the previous CSS is removed + + currentPageStyle = document.createElement('style'); + currentPageStyle.type = 'text/css'; + currentPageStyle.appendChild(document.createTextNode(cssContent)); + document.head.appendChild(currentPageStyle); +} + +function removeCss() { + if (currentPageStyle) { + document.head.removeChild(currentPageStyle); + currentPageStyle = null; + } +} + +async function getProcesses(): Promise> { + const service = await Services.getInstance(); + const processes = await service.getProcesses(); + return processes; +} diff --git a/src/router.ts b/src/router.ts index af04a31..b662d2c 100755 --- a/src/router.ts +++ b/src/router.ts @@ -1,950 +1,950 @@ -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.js'; - -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('process'); - } 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); - - if (!result) { - const errorMsg = 'Failed to pair device: User refused to link'; - 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 handleCreatePairing = async (event: MessageEvent) => { - if (event.data.type !== MessageType.CREATE_PAIRING) { - return; - } - - if (services.isPaired()) { - const errorMsg = 'Device already 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'); - } - - console.log('🚀 Starting pairing process'); - const myAddress = services.getDeviceAddress(); - const createPairingProcessReturn = await services.createPairingProcess('', [myAddress]); - const pairingId = createPairingProcessReturn.updated_process?.process_id; - const stateId = createPairingProcessReturn.updated_process?.current_process?.states[0]?.state_id as string; - services.pairDevice(pairingId, [myAddress]); - await services.handleApiReturn(createPairingProcessReturn); - - const createPrdUpdateReturn = await services.createPrdUpdate(pairingId, stateId); - await services.handleApiReturn(createPrdUpdateReturn); - const approveChangeReturn = await services.approveChange(pairingId, stateId); - await services.handleApiReturn(approveChangeReturn); - - await services.confirmPairing(); - - // Send success response - const successMsg = { - type: MessageType.PAIRING_CREATED, - pairingId, - messageId: event.data.messageId - }; - window.parent.postMessage(successMsg, event.origin); - } catch (e) { - const errorMsg = `Failed to create pairing process: ${e}`; - 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); - - await services.checkConnections(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); - } - } - - 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.CREATE_PAIRING: - await handleCreatePairing(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; - 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 || ''); - } - } -})); +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.js'; + +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('process'); + } 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); + + if (!result) { + const errorMsg = 'Failed to pair device: User refused to link'; + 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 handleCreatePairing = async (event: MessageEvent) => { + if (event.data.type !== MessageType.CREATE_PAIRING) { + return; + } + + if (services.isPaired()) { + const errorMsg = 'Device already 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'); + } + + console.log('🚀 Starting pairing process'); + const myAddress = services.getDeviceAddress(); + const createPairingProcessReturn = await services.createPairingProcess('', [myAddress]); + const pairingId = createPairingProcessReturn.updated_process?.process_id; + const stateId = createPairingProcessReturn.updated_process?.current_process?.states[0]?.state_id as string; + services.pairDevice(pairingId, [myAddress]); + await services.handleApiReturn(createPairingProcessReturn); + + const createPrdUpdateReturn = await services.createPrdUpdate(pairingId, stateId); + await services.handleApiReturn(createPrdUpdateReturn); + const approveChangeReturn = await services.approveChange(pairingId, stateId); + await services.handleApiReturn(approveChangeReturn); + + await services.confirmPairing(); + + // Send success response + const successMsg = { + type: MessageType.PAIRING_CREATED, + pairingId, + messageId: event.data.messageId + }; + window.parent.postMessage(successMsg, event.origin); + } catch (e) { + const errorMsg = `Failed to create pairing process: ${e}`; + 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); + + await services.checkConnections(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); + } + } + + 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.CREATE_PAIRING: + await handleCreatePairing(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; + 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 || ''); + } + } +})); diff --git a/src/services/modal.service.ts b/src/services/modal.service.ts index c3b48f0..0a9584e 100755 --- a/src/services/modal.service.ts +++ b/src/services/modal.service.ts @@ -1,230 +1,230 @@ -import modalHtml from '../components/login-modal/login-modal.html?raw'; -import modalScript from '../components/login-modal/login-modal.js?raw'; -import validationModalStyle from '../components/validation-modal/validation-modal.css?raw'; -import Services from './service'; -import { init, navigate } from '../router'; -import { addressToEmoji } from '../utils/sp-address.utils'; -import { RoleDefinition } from '../../pkg/sdk_client.js'; -import { initValidationModal } from '~/components/validation-modal/validation-modal'; -import { interpolate } from '~/utils/html.utils'; - -interface ConfirmationModalOptions { - title: string; - content: string; - confirmText?: string; - cancelText?: string; -} - -export default class ModalService { - private static instance: ModalService; - private stateId: string | null = null; - private processId: string | null = null; - private constructor() {} - private paired_addresses: string[] = []; - private modal: HTMLElement | null = null; - - // Method to access the singleton instance of Services - public static async getInstance(): Promise { - if (!ModalService.instance) { - ModalService.instance = new ModalService(); - } - return ModalService.instance; - } - - public openLoginModal(myAddress: string, receiverAddress: string) { - const container = document.querySelector('.page-container'); - let html = modalHtml; - html = html.replace('{{device1}}', myAddress); - html = html.replace('{{device2}}', receiverAddress); - if (container) container.innerHTML += html; - const modal = document.getElementById('login-modal'); - if (modal) modal.style.display = 'flex'; - const newScript = document.createElement('script'); - - newScript.setAttribute('type', 'module'); - newScript.textContent = modalScript; - document.head.appendChild(newScript).parentNode?.removeChild(newScript); - } - - async injectModal(members: any[]) { - const container = document.querySelector('#containerId'); - if (container) { - let html = await fetch('/src/components/modal/confirmation-modal.html').then((res) => res.text()); - html = html.replace('{{device1}}', await addressToEmoji(members[0]['sp_addresses'][0])); - html = html.replace('{{device2}}', await addressToEmoji(members[0]['sp_addresses'][1])); - container.innerHTML += html; - - // Dynamically load the header JS - const script = document.createElement('script'); - script.src = '/src/components/modal/confirmation-modal.ts'; - script.type = 'module'; - document.head.appendChild(script); - } - } - - async injectCreationModal(members: any[]) { - const container = document.querySelector('#containerId'); - if (container) { - let html = await fetch('/src/components/modal/creation-modal.html').then((res) => res.text()); - html = html.replace('{{device1}}', await addressToEmoji(members[0]['sp_addresses'][0])); - container.innerHTML += html; - - // Dynamically load the header JS - const script = document.createElement('script'); - script.src = '/src/components/modal/confirmation-modal.ts'; - script.type = 'module'; - document.head.appendChild(script); - } - } - - // Device 1 wait Device 2 - async injectWaitingModal() { - const container = document.querySelector('#containerId'); - if (container) { - let html = await fetch('/src/components/modal/waiting-modal.html').then((res) => res.text()); - container.innerHTML += html; - } - } - - async injectValidationModal(processDiff: any) { - const container = document.querySelector('#containerId'); - if (container) { - let html = await fetch('/src/components/validation-modal/validation-modal.html').then((res) => res.text()); - html = interpolate(html, {processId: processDiff.processId}) - container.innerHTML += html; - - // Dynamically load the header JS - const script = document.createElement('script'); - script.id = 'validation-modal-script'; - script.src = '/src/components/validation-modal/validation-modal.ts'; - script.type = 'module'; - document.head.appendChild(script); - const css = document.createElement('style'); - css.id = 'validation-modal-css'; - css.innerText = validationModalStyle; - document.head.appendChild(css); - initValidationModal(processDiff) - } - } - - async closeValidationModal() { - const script = document.querySelector('#validation-modal-script'); - const css = document.querySelector('#validation-modal-css'); - const component = document.querySelector('#validation-modal'); - script?.remove(); - css?.remove(); - component?.remove(); - } - - public async openPairingConfirmationModal(roleDefinition: Record, processId: string, stateId: string) { - let members; - if (roleDefinition['pairing']) { - const owner = roleDefinition['pairing']; - members = owner.members; - } else { - throw new Error('No "pairing" role'); - } - - if (members.length != 1) { - throw new Error('Must have exactly 1 member'); - } - - console.log("MEMBERS:", members); - // We take all the addresses except our own - const service = await Services.getInstance(); - const localAddress = service.getDeviceAddress(); - for (const member of members) { - if (member.sp_addresses) { - for (const address of member.sp_addresses) { - if (address !== localAddress) { - this.paired_addresses.push(address); - } - } - } - } - this.processId = processId; - this.stateId = stateId; - - if (members[0].sp_addresses.length === 1) { - await this.injectCreationModal(members); - this.modal = document.getElementById('creation-modal'); - console.log("LENGTH:", members[0].sp_addresses.length); - } else { - await this.injectModal(members); - this.modal = document.getElementById('modal'); - console.log("LENGTH:", members[0].sp_addresses.length); - } - - if (this.modal) this.modal.style.display = 'flex'; - - // Close modal when clicking outside of it - window.onclick = (event) => { - if (event.target === this.modal) { - this.closeConfirmationModal(); - } - }; - } - confirmLogin() { - console.log('=============> Confirm Login'); - } - async closeLoginModal() { - if (this.modal) this.modal.style.display = 'none'; - } - - async showConfirmationModal(options: ConfirmationModalOptions, fullscreen: boolean = false): Promise { - // Create modal element - const modalElement = document.createElement('div'); - modalElement.id = 'confirmation-modal'; - modalElement.innerHTML = ` - - `; - - // Add modal to document - document.body.appendChild(modalElement); - - // Return promise that resolves with user choice - return new Promise((resolve) => { - const confirmButton = modalElement.querySelector('#confirm-button'); - const cancelButton = modalElement.querySelector('#cancel-button'); - const modalOverlay = modalElement.querySelector('.modal-overlay'); - - const cleanup = () => { - modalElement.remove(); - }; - - confirmButton?.addEventListener('click', () => { - cleanup(); - resolve(true); - }); - - cancelButton?.addEventListener('click', () => { - cleanup(); - resolve(false); - }); - - modalOverlay?.addEventListener('click', (e) => { - if (e.target === modalOverlay) { - cleanup(); - resolve(false); - } - }); - }); - } - - async closeConfirmationModal() { - const service = await Services.getInstance(); - await service.unpairDevice(); - if (this.modal) this.modal.style.display = 'none'; - } -} +import modalHtml from '../components/login-modal/login-modal.html?raw'; +import modalScript from '../components/login-modal/login-modal.js?raw'; +import validationModalStyle from '../components/validation-modal/validation-modal.css?raw'; +import Services from './service'; +import { init, navigate } from '../router'; +import { addressToEmoji } from '../utils/sp-address.utils'; +import { RoleDefinition } from '../../pkg/sdk_client.js'; +import { initValidationModal } from '~/components/validation-modal/validation-modal'; +import { interpolate } from '~/utils/html.utils'; + +interface ConfirmationModalOptions { + title: string; + content: string; + confirmText?: string; + cancelText?: string; +} + +export default class ModalService { + private static instance: ModalService; + private stateId: string | null = null; + private processId: string | null = null; + private constructor() {} + private paired_addresses: string[] = []; + private modal: HTMLElement | null = null; + + // Method to access the singleton instance of Services + public static async getInstance(): Promise { + if (!ModalService.instance) { + ModalService.instance = new ModalService(); + } + return ModalService.instance; + } + + public openLoginModal(myAddress: string, receiverAddress: string) { + const container = document.querySelector('.page-container'); + let html = modalHtml; + html = html.replace('{{device1}}', myAddress); + html = html.replace('{{device2}}', receiverAddress); + if (container) container.innerHTML += html; + const modal = document.getElementById('login-modal'); + if (modal) modal.style.display = 'flex'; + const newScript = document.createElement('script'); + + newScript.setAttribute('type', 'module'); + newScript.textContent = modalScript; + document.head.appendChild(newScript).parentNode?.removeChild(newScript); + } + + async injectModal(members: any[]) { + const container = document.querySelector('#containerId'); + if (container) { + let html = await fetch('/src/components/modal/confirmation-modal.html').then((res) => res.text()); + html = html.replace('{{device1}}', await addressToEmoji(members[0]['sp_addresses'][0])); + html = html.replace('{{device2}}', await addressToEmoji(members[0]['sp_addresses'][1])); + container.innerHTML += html; + + // Dynamically load the header JS + const script = document.createElement('script'); + script.src = '/src/components/modal/confirmation-modal.ts'; + script.type = 'module'; + document.head.appendChild(script); + } + } + + async injectCreationModal(members: any[]) { + const container = document.querySelector('#containerId'); + if (container) { + let html = await fetch('/src/components/modal/creation-modal.html').then((res) => res.text()); + html = html.replace('{{device1}}', await addressToEmoji(members[0]['sp_addresses'][0])); + container.innerHTML += html; + + // Dynamically load the header JS + const script = document.createElement('script'); + script.src = '/src/components/modal/confirmation-modal.ts'; + script.type = 'module'; + document.head.appendChild(script); + } + } + + // Device 1 wait Device 2 + async injectWaitingModal() { + const container = document.querySelector('#containerId'); + if (container) { + let html = await fetch('/src/components/modal/waiting-modal.html').then((res) => res.text()); + container.innerHTML += html; + } + } + + async injectValidationModal(processDiff: any) { + const container = document.querySelector('#containerId'); + if (container) { + let html = await fetch('/src/components/validation-modal/validation-modal.html').then((res) => res.text()); + html = interpolate(html, {processId: processDiff.processId}) + container.innerHTML += html; + + // Dynamically load the header JS + const script = document.createElement('script'); + script.id = 'validation-modal-script'; + script.src = '/src/components/validation-modal/validation-modal.ts'; + script.type = 'module'; + document.head.appendChild(script); + const css = document.createElement('style'); + css.id = 'validation-modal-css'; + css.innerText = validationModalStyle; + document.head.appendChild(css); + initValidationModal(processDiff) + } + } + + async closeValidationModal() { + const script = document.querySelector('#validation-modal-script'); + const css = document.querySelector('#validation-modal-css'); + const component = document.querySelector('#validation-modal'); + script?.remove(); + css?.remove(); + component?.remove(); + } + + public async openPairingConfirmationModal(roleDefinition: Record, processId: string, stateId: string) { + let members; + if (roleDefinition['pairing']) { + const owner = roleDefinition['pairing']; + members = owner.members; + } else { + throw new Error('No "pairing" role'); + } + + if (members.length != 1) { + throw new Error('Must have exactly 1 member'); + } + + console.log("MEMBERS:", members); + // We take all the addresses except our own + const service = await Services.getInstance(); + const localAddress = service.getDeviceAddress(); + for (const member of members) { + if (member.sp_addresses) { + for (const address of member.sp_addresses) { + if (address !== localAddress) { + this.paired_addresses.push(address); + } + } + } + } + this.processId = processId; + this.stateId = stateId; + + if (members[0].sp_addresses.length === 1) { + await this.injectCreationModal(members); + this.modal = document.getElementById('creation-modal'); + console.log("LENGTH:", members[0].sp_addresses.length); + } else { + await this.injectModal(members); + this.modal = document.getElementById('modal'); + console.log("LENGTH:", members[0].sp_addresses.length); + } + + if (this.modal) this.modal.style.display = 'flex'; + + // Close modal when clicking outside of it + window.onclick = (event) => { + if (event.target === this.modal) { + this.closeConfirmationModal(); + } + }; + } + confirmLogin() { + console.log('=============> Confirm Login'); + } + async closeLoginModal() { + if (this.modal) this.modal.style.display = 'none'; + } + + async showConfirmationModal(options: ConfirmationModalOptions, fullscreen: boolean = false): Promise { + // Create modal element + const modalElement = document.createElement('div'); + modalElement.id = 'confirmation-modal'; + modalElement.innerHTML = ` + + `; + + // Add modal to document + document.body.appendChild(modalElement); + + // Return promise that resolves with user choice + return new Promise((resolve) => { + const confirmButton = modalElement.querySelector('#confirm-button'); + const cancelButton = modalElement.querySelector('#cancel-button'); + const modalOverlay = modalElement.querySelector('.modal-overlay'); + + const cleanup = () => { + modalElement.remove(); + }; + + confirmButton?.addEventListener('click', () => { + cleanup(); + resolve(true); + }); + + cancelButton?.addEventListener('click', () => { + cleanup(); + resolve(false); + }); + + modalOverlay?.addEventListener('click', (e) => { + if (e.target === modalOverlay) { + cleanup(); + resolve(false); + } + }); + }); + } + + async closeConfirmationModal() { + const service = await Services.getInstance(); + await service.unpairDevice(); + if (this.modal) this.modal.style.display = 'none'; + } +} diff --git a/src/utils/sp-address.utils.ts b/src/utils/sp-address.utils.ts index f62b2da..1e8f0fd 100755 --- a/src/utils/sp-address.utils.ts +++ b/src/utils/sp-address.utils.ts @@ -1,213 +1,213 @@ -import Services from '../services/service'; -import { getCorrectDOM } from './html.utils'; -import { addSubscription } from './subscription.utils'; -import QRCode from 'qrcode'; - -//Copy Address -export async function copyToClipboard(fullAddress: string) { - try { - await navigator.clipboard.writeText(fullAddress); - alert('Adresse copiée dans le presse-papiers !'); - } catch (err) { - console.error('Failed to copy the address: ', err); - } -} - -//Generate emojis list -export function generateEmojiList(): string[] { - const emojiRanges = [ - [0x1f600, 0x1f64f], - [0x1f300, 0x1f5ff], - [0x1f680, 0x1f6ff], - [0x1f700, 0x1f77f], - ]; - - const emojiList: string[] = []; - for (const range of emojiRanges) { - const [start, end] = range; - for (let i = start; i <= end && emojiList.length < 256; i++) { - emojiList.push(String.fromCodePoint(i)); - } - if (emojiList.length >= 256) { - break; - } - } - - return emojiList.slice(0, 256); -} - -//Adress to emojis -export async function addressToEmoji(text: string): Promise { - //Adress to Hash - const encoder = new TextEncoder(); - const data = encoder.encode(text); - const hashBuffer = await crypto.subtle.digest('SHA-256', data); - - const hash = new Uint8Array(hashBuffer); - const bytes = hash.slice(-4); - - //Hash slice to emojis - const emojiList = generateEmojiList(); - const emojis = Array.from(bytes) - .map((byte) => emojiList[byte]) - .join(''); - return emojis; -} - -//Get emojis from other device -async function emojisPairingRequest() { - try { - const container = getCorrectDOM('login-4nk-component') as HTMLElement; - - const urlParams: URLSearchParams = new URLSearchParams(window.location.search); - const sp_adress: string | null = urlParams.get('sp_address'); - - if (!sp_adress) { - // console.error("No 'sp_adress' parameter found in the URL."); - return; - } - - const emojis = await addressToEmoji(sp_adress); - const emojiDisplay = container?.querySelector('.pairing-request'); - - if (emojiDisplay) { - emojiDisplay.textContent = '(Request from: ' + emojis + ')'; - } - } catch (err) { - console.error(err); - } -} - -// Display address emojis and other device emojis -export async function displayEmojis(text: string) { - console.log('🚀 ~ Services ~ adressToEmoji'); - try { - const container = getCorrectDOM('login-4nk-component') as HTMLElement; - const emojis = await addressToEmoji(text); - const emojiDisplay = container?.querySelector('.emoji-display'); - - if (emojiDisplay) { - emojiDisplay.textContent = emojis; - } - - emojisPairingRequest(); - - initAddressInput(); - } catch (err) { - console.error(err); - } -} - -// Verify Other address -export function initAddressInput() { - const container = getCorrectDOM('login-4nk-component') as HTMLElement - const addressInput = container.querySelector('#addressInput') as HTMLInputElement; - const emojiDisplay = container.querySelector('#emoji-display-2'); - const okButton = container.querySelector('#okButton') as HTMLButtonElement; - const createButton = container.querySelector('#createButton') as HTMLButtonElement; - const actionButton = container.querySelector('#actionButton') as HTMLButtonElement; - addSubscription(addressInput, 'input', async () => { - let address = addressInput.value; - - // Vérifie si l'adresse est une URL - try { - const url = new URL(address); - // Si c'est une URL valide, extraire le paramètre sp_address - const urlParams = new URLSearchParams(url.search); - const extractedAddress = urlParams.get('sp_address') || ''; // Prend sp_address ou une chaîne vide - - if (extractedAddress) { - address = extractedAddress; - addressInput.value = address; // Met à jour l'input pour afficher uniquement l'adresse extraite - } - } catch (e) { - // Si ce n'est pas une URL valide, on garde l'adresse originale - console.log("Ce n'est pas une URL valide, on garde l'adresse originale."); - } - if (address) { - const emojis = await addressToEmoji(address); - if (emojiDisplay) { - emojiDisplay.innerHTML = emojis; - } - if (okButton) { - okButton.style.display = 'inline-block'; - } - } else { - if (emojiDisplay) { - emojiDisplay.innerHTML = ''; - } - if (okButton) { - okButton.style.display = 'none'; - } - } - }); - - if (createButton) { - addSubscription(createButton, 'click', () => { - onCreateButtonClick(); - }); - } -} - -async function onCreateButtonClick() { - try { - await prepareAndSendPairingTx(); - const service = await Services.getInstance(); - await service.confirmPairing(); - } catch (e) { - console.error(`onCreateButtonClick error: ${e}`); - } -} - -export async function prepareAndSendPairingTx(): Promise { - const service = await Services.getInstance(); - - // checkConnections requires a Process object, not an empty array - // This call has been removed as it was causing TypeScript errors - - try { - const relayAddress = service.getAllRelays(); - const createPairingProcessReturn = await service.createPairingProcess( - "", - [], - ); - - if (!createPairingProcessReturn.updated_process) { - throw new Error('createPairingProcess returned an empty new process'); - } - - service.setProcessId(createPairingProcessReturn.updated_process.process_id); - service.setStateId(createPairingProcessReturn.updated_process.current_process.states[0].state_id); - - await service.handleApiReturn(createPairingProcessReturn); - - } catch (err) { - console.error(err); - } -} - -export async function generateQRCode(spAddress: string) { - try { - const container = getCorrectDOM('login-4nk-component') as HTMLElement - const currentUrl = 'https://' + window.location.host; - const url = await QRCode.toDataURL(currentUrl + '?sp_address=' + spAddress); - const qrCode = container?.querySelector('.qr-code img'); - qrCode?.setAttribute('src', url); - } catch (err) { - console.error(err); - } -} - -export async function generateCreateBtn() { - try{ - //Generate CreateBtn - const container = getCorrectDOM('login-4nk-component') as HTMLElement - const createBtn = container?.querySelector('.create-btn'); - if (createBtn) { - createBtn.textContent = 'CREATE'; - } - } catch (err) { - console.error(err); - } - +import Services from '../services/service'; +import { getCorrectDOM } from './html.utils'; +import { addSubscription } from './subscription.utils'; +import QRCode from 'qrcode'; + +//Copy Address +export async function copyToClipboard(fullAddress: string) { + try { + await navigator.clipboard.writeText(fullAddress); + alert('Adresse copiée dans le presse-papiers !'); + } catch (err) { + console.error('Failed to copy the address: ', err); + } +} + +//Generate emojis list +export function generateEmojiList(): string[] { + const emojiRanges = [ + [0x1f600, 0x1f64f], + [0x1f300, 0x1f5ff], + [0x1f680, 0x1f6ff], + [0x1f700, 0x1f77f], + ]; + + const emojiList: string[] = []; + for (const range of emojiRanges) { + const [start, end] = range; + for (let i = start; i <= end && emojiList.length < 256; i++) { + emojiList.push(String.fromCodePoint(i)); + } + if (emojiList.length >= 256) { + break; + } + } + + return emojiList.slice(0, 256); +} + +//Adress to emojis +export async function addressToEmoji(text: string): Promise { + //Adress to Hash + const encoder = new TextEncoder(); + const data = encoder.encode(text); + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + + const hash = new Uint8Array(hashBuffer); + const bytes = hash.slice(-4); + + //Hash slice to emojis + const emojiList = generateEmojiList(); + const emojis = Array.from(bytes) + .map((byte) => emojiList[byte]) + .join(''); + return emojis; +} + +//Get emojis from other device +async function emojisPairingRequest() { + try { + const container = getCorrectDOM('login-4nk-component') as HTMLElement; + + const urlParams: URLSearchParams = new URLSearchParams(window.location.search); + const sp_adress: string | null = urlParams.get('sp_address'); + + if (!sp_adress) { + // console.error("No 'sp_adress' parameter found in the URL."); + return; + } + + const emojis = await addressToEmoji(sp_adress); + const emojiDisplay = container?.querySelector('.pairing-request'); + + if (emojiDisplay) { + emojiDisplay.textContent = '(Request from: ' + emojis + ')'; + } + } catch (err) { + console.error(err); + } +} + +// Display address emojis and other device emojis +export async function displayEmojis(text: string) { + console.log('🚀 ~ Services ~ adressToEmoji'); + try { + const container = getCorrectDOM('login-4nk-component') as HTMLElement; + const emojis = await addressToEmoji(text); + const emojiDisplay = container?.querySelector('.emoji-display'); + + if (emojiDisplay) { + emojiDisplay.textContent = emojis; + } + + emojisPairingRequest(); + + initAddressInput(); + } catch (err) { + console.error(err); + } +} + +// Verify Other address +export function initAddressInput() { + const container = getCorrectDOM('login-4nk-component') as HTMLElement + const addressInput = container.querySelector('#addressInput') as HTMLInputElement; + const emojiDisplay = container.querySelector('#emoji-display-2'); + const okButton = container.querySelector('#okButton') as HTMLButtonElement; + const createButton = container.querySelector('#createButton') as HTMLButtonElement; + const actionButton = container.querySelector('#actionButton') as HTMLButtonElement; + addSubscription(addressInput, 'input', async () => { + let address = addressInput.value; + + // Vérifie si l'adresse est une URL + try { + const url = new URL(address); + // Si c'est une URL valide, extraire le paramètre sp_address + const urlParams = new URLSearchParams(url.search); + const extractedAddress = urlParams.get('sp_address') || ''; // Prend sp_address ou une chaîne vide + + if (extractedAddress) { + address = extractedAddress; + addressInput.value = address; // Met à jour l'input pour afficher uniquement l'adresse extraite + } + } catch (e) { + // Si ce n'est pas une URL valide, on garde l'adresse originale + console.log("Ce n'est pas une URL valide, on garde l'adresse originale."); + } + if (address) { + const emojis = await addressToEmoji(address); + if (emojiDisplay) { + emojiDisplay.innerHTML = emojis; + } + if (okButton) { + okButton.style.display = 'inline-block'; + } + } else { + if (emojiDisplay) { + emojiDisplay.innerHTML = ''; + } + if (okButton) { + okButton.style.display = 'none'; + } + } + }); + + if (createButton) { + addSubscription(createButton, 'click', () => { + onCreateButtonClick(); + }); + } +} + +async function onCreateButtonClick() { + try { + await prepareAndSendPairingTx(); + const service = await Services.getInstance(); + await service.confirmPairing(); + } catch (e) { + console.error(`onCreateButtonClick error: ${e}`); + } +} + +export async function prepareAndSendPairingTx(): Promise { + const service = await Services.getInstance(); + + // checkConnections requires a Process object, not an empty array + // This call has been removed as it was causing TypeScript errors + + try { + const relayAddress = service.getAllRelays(); + const createPairingProcessReturn = await service.createPairingProcess( + "", + [], + ); + + if (!createPairingProcessReturn.updated_process) { + throw new Error('createPairingProcess returned an empty new process'); + } + + service.setProcessId(createPairingProcessReturn.updated_process.process_id); + service.setStateId(createPairingProcessReturn.updated_process.current_process.states[0].state_id); + + await service.handleApiReturn(createPairingProcessReturn); + + } catch (err) { + console.error(err); + } +} + +export async function generateQRCode(spAddress: string) { + try { + const container = getCorrectDOM('login-4nk-component') as HTMLElement + const currentUrl = 'https://' + window.location.host; + const url = await QRCode.toDataURL(currentUrl + '?sp_address=' + spAddress); + const qrCode = container?.querySelector('.qr-code img'); + qrCode?.setAttribute('src', url); + } catch (err) { + console.error(err); + } +} + +export async function generateCreateBtn() { + try{ + //Generate CreateBtn + const container = getCorrectDOM('login-4nk-component') as HTMLElement + const createBtn = container?.querySelector('.create-btn'); + if (createBtn) { + createBtn.textContent = 'CREATE'; + } + } catch (err) { + console.error(err); + } + } \ No newline at end of file diff --git a/src/websockets.ts b/src/websockets.ts index e171df3..a44fe73 100755 --- a/src/websockets.ts +++ b/src/websockets.ts @@ -1,89 +1,89 @@ -import { AnkFlag } from '../../pkg/sdk_client.js'; -import Services from './services/service'; - -let ws: WebSocket; -let messageQueue: string[] = []; -export async function initWebsocket(url: string) { - ws = new WebSocket(url); - - if (ws !== null) { - ws.onopen = async (event) => { - console.log('WebSocket connection established'); - - while (messageQueue.length > 0) { - const message = messageQueue.shift(); - if (message) { - ws.send(message); - } - } - }; - - // Listen for messages - ws.onmessage = (event) => { - const msgData = event.data; - - // console.log("Received text message: ", msgData); - (async () => { - if (typeof msgData === 'string') { - try { - const parsedMessage = JSON.parse(msgData); - const services = await Services.getInstance(); - switch (parsedMessage.flag) { - case 'Handshake': - await services.handleHandshakeMsg(url, parsedMessage.content); - break; - case 'NewTx': - await services.parseNewTx(parsedMessage.content); - break; - case 'Cipher': - await services.parseCipher(parsedMessage.content); - break; - case 'Commit': - // Basically if we see this it means we have an error - await services.handleCommitError(parsedMessage.content); - break; - } - } catch (error) { - console.error('Received an invalid message:', error); - } - } else { - console.error('Received a non-string message'); - } - })(); - }; - - // Listen for possible errors - ws.onerror = (event) => { - console.error('WebSocket error:', event); - }; - - // Listen for when the connection is closed - ws.onclose = (event) => { - console.log('WebSocket is closed now.'); - }; - } -} - -// Method to send messages -export function sendMessage(flag: AnkFlag, message: string): void { - if (ws.readyState === WebSocket.OPEN) { - const networkMessage = { - flag: flag, - content: message, - }; - console.log('Sending message of type:', flag); - ws.send(JSON.stringify(networkMessage)); - } else { - console.error('WebSocket is not open. ReadyState:', ws.readyState); - messageQueue.push(message); - } -} - -export function getUrl(): string { - return ws.url; -} - -// Method to close the WebSocket connection -export function close(): void { - ws.close(); -} +import { AnkFlag } from '../../pkg/sdk_client.js'; +import Services from './services/service'; + +let ws: WebSocket; +let messageQueue: string[] = []; +export async function initWebsocket(url: string) { + ws = new WebSocket(url); + + if (ws !== null) { + ws.onopen = async (event) => { + console.log('WebSocket connection established'); + + while (messageQueue.length > 0) { + const message = messageQueue.shift(); + if (message) { + ws.send(message); + } + } + }; + + // Listen for messages + ws.onmessage = (event) => { + const msgData = event.data; + + // console.log("Received text message: ", msgData); + (async () => { + if (typeof msgData === 'string') { + try { + const parsedMessage = JSON.parse(msgData); + const services = await Services.getInstance(); + switch (parsedMessage.flag) { + case 'Handshake': + await services.handleHandshakeMsg(url, parsedMessage.content); + break; + case 'NewTx': + await services.parseNewTx(parsedMessage.content); + break; + case 'Cipher': + await services.parseCipher(parsedMessage.content); + break; + case 'Commit': + // Basically if we see this it means we have an error + await services.handleCommitError(parsedMessage.content); + break; + } + } catch (error) { + console.error('Received an invalid message:', error); + } + } else { + console.error('Received a non-string message'); + } + })(); + }; + + // Listen for possible errors + ws.onerror = (event) => { + console.error('WebSocket error:', event); + }; + + // Listen for when the connection is closed + ws.onclose = (event) => { + console.log('WebSocket is closed now.'); + }; + } +} + +// Method to send messages +export function sendMessage(flag: AnkFlag, message: string): void { + if (ws.readyState === WebSocket.OPEN) { + const networkMessage = { + flag: flag, + content: message, + }; + console.log('Sending message of type:', flag); + ws.send(JSON.stringify(networkMessage)); + } else { + console.error('WebSocket is not open. ReadyState:', ws.readyState); + messageQueue.push(message); + } +} + +export function getUrl(): string { + return ws.url; +} + +// Method to close the WebSocket connection +export function close(): void { + ws.close(); +}