ihm_client/src/router.ts
NicolasCantu 274b19e410 chore: align relay bootstrap configuration
**Motivations :**
- document and enforce the secure websocket endpoint for the relay
- streamline wallet setup flow without manual blockers

**Modifications :**
- update docs and service bootstrap logic to rely on VITE_BOOTSTRAPURL with explicit /ws suffix
- clean wallet setup and block sync pages to run automatically without continue buttons
- refresh shared styles and services to match the new initialization path and logging adjustments

**Page affectées :**
- docs/INTEGRATION.md
- src/services/service.ts
- src/pages/wallet-setup/wallet-setup.*
2025-10-31 13:46:59 +01:00

1394 lines
47 KiB
TypeScript
Executable File

// CSS is loaded via HTML link tag
import Database from './services/database.service';
import { secureLogger } from './services/secure-logger';
import Services from './services/service';
import TokenService from './services/token';
import { cleanSubscriptions } from './utils/subscription.utils';
import { LoginComponent } from './pages/home/home-component';
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 { checkPBKDF2Key } from './utils/prerequisites.utils';
const routes: { [key: string]: string } = {
home: '/src/pages/home/home.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',
'security-setup': '/src/pages/security-setup/security-setup.html',
'wallet-setup': '/src/pages/wallet-setup/wallet-setup.html',
'birthday-setup': '/src/pages/birthday-setup/birthday-setup.html',
'block-sync': '/src/pages/block-sync/block-sync.html',
pairing: '/src/pages/pairing/pairing.html',
};
export let currentRoute = '';
/**
* Vérifie l'état du storage et détermine quelle page afficher selon l'état actuel
* Version légère qui n'initialise pas WebAssembly pour la première visite
* Logique de progression :
* - Si pairing → account
* - Si date anniversaire → pairing
* - Si wallet → birthday-setup
* - Si pbkdf2 → wallet-setup
* - Sinon → security-setup
*/
export async function checkStorageStateAndNavigate(): Promise<void> {
secureLogger.debug('🔍 Checking storage state to determine next step...', {
component: 'Router',
});
try {
// Utiliser DeviceReaderService pour éviter d'initialiser WebAssembly
const { DeviceReaderService } = await import('./services/device-reader.service');
const deviceReader = DeviceReaderService.getInstance();
// Vérifier si une clé PBKDF2 existe (vérification la plus légère)
const pbkdf2KeyResult = await checkPBKDF2Key();
if (!pbkdf2KeyResult) {
// Aucune clé PBKDF2 trouvée, commencer par la configuration de sécurité
secureLogger.info('🔐 No PBKDF2 key found, navigating to security-setup', {
component: 'Router',
});
await navigate('security-setup');
return;
}
// Vérifier si le wallet existe (même avec birthday = 0)
const device = await deviceReader.getDeviceFromDatabase();
if (!device?.sp_wallet) {
secureLogger.info('💰 Wallet does not exist, navigating to wallet-setup', {
component: 'Router',
});
await navigate('wallet-setup');
return;
}
// Vérifier si la date anniversaire est configurée (wallet avec birthday > 0)
if (device.sp_wallet.birthday && device.sp_wallet.birthday > 0) {
secureLogger.info('🎂 Birthday is configured, navigating to pairing', {
component: 'Router',
});
// Rediriger vers la page de pairing standalone
await navigate('pairing');
return;
}
// Wallet existe mais birthday pas configuré
secureLogger.info('💰 Wallet exists but birthday not set, navigating to birthday-setup', {
component: 'Router',
});
await navigate('birthday-setup');
return;
} catch (error) {
secureLogger.error('❌ Error checking storage state:', error, { component: 'Router' });
// En cas d'erreur, commencer par la configuration de sécurité
secureLogger.error('🔐 Error occurred, defaulting to security-setup', { component: 'Router' });
await navigate('security-setup');
}
}
export async function navigate(path: string) {
secureLogger.info('🧭 Navigate called with path:', { component: 'Router', data: path });
cleanSubscriptions();
cleanPage();
path = path.replace(/^\//, '');
if (path.includes('/')) {
const parsedPath = path.split('/')[0];
if (!routes[parsedPath]) {
path = 'home';
}
}
secureLogger.info('🧭 Final path after processing:', { component: 'Router', data: path });
await handleLocation(path);
secureLogger.info('🧭 handleLocation completed for path:', { component: 'Router', data: path });
}
async function handleLocation(path: string) {
secureLogger.info('📍 handleLocation called with path:', { component: 'Router', data: path });
const parsedPath = path.split('/');
if (path.includes('/')) {
path = parsedPath[0];
}
currentRoute = path;
const routeHtml = routes[path] || routes['home'];
secureLogger.info('📍 Current route set to:', { component: 'Router', data: currentRoute });
secureLogger.info('📍 Route HTML:', { component: 'Router', data: routeHtml });
// Pour les pages de setup, rediriger directement vers la page HTML
if (
path === 'security-setup' ||
path === 'wallet-setup' ||
path === 'birthday-setup' ||
path === 'block-sync' ||
path === 'pairing'
) {
secureLogger.info('📍 Processing setup route:', { component: 'Router', data: path });
window.location.href = routeHtml;
return;
}
const content = document.getElementById('containerId');
secureLogger.info('📍 Container element found:', { component: 'Router', data: !!content });
if (content) {
if (path === 'home') {
secureLogger.info('🏠 Processing home route...', { component: 'Router' });
// Use LoginComponent
// const loginComponent = LoginComponent; // Used as type, not variable
const container = document.querySelector('#containerId');
secureLogger.info('🏠 Container for home:', { component: 'Router', data: !!container });
const accountComponent = document.createElement('login-4nk-component');
accountComponent.setAttribute(
'style',
'width: 100vw; height: 100vh; position: relative; grid-row: 2;'
);
if (container) {
container.appendChild(accountComponent);
secureLogger.info('🏠 Component appended to container', { component: 'Router' });
}
// Initialize the home page after component is added to DOM
secureLogger.info('🏠 Initializing home page...', { component: 'Router' });
try {
const { initHomePage } = await import('./pages/home/home');
await initHomePage();
secureLogger.info('✅ Home page initialized successfully', { component: 'Router' });
} catch (error) {
secureLogger.error('❌ Failed to initialize home page:', error, { component: 'Router' });
}
} else {
secureLogger.info('📍 Processing other route:', { component: 'Router', data: path });
const html = await fetch(routeHtml).then(data => data.text());
content.innerHTML = html;
}
await new Promise(requestAnimationFrame);
// Essential functions are now handled directly in the application
// const modalService = await ModalService.getInstance()
// modalService.injectValidationModal()
switch (path) {
case 'process':
// Process functionality removed - redirect to account
secureLogger.warn('Process functionality has been removed, redirecting to account', {
component: 'Router',
});
await navigate('account');
break;
case 'process-element':
// Process element functionality removed
secureLogger.warn('Process element functionality has been removed', {
component: 'Router',
});
break;
case 'account':
// Account page now uses device-management component directly
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':
// Signature functionality removed
secureLogger.warn('Signature functionality has been removed', { component: 'Router' });
break;
}
}
}
window.onpopstate = async () => {
const services = await Services.getInstance();
if (!services.isPaired()) {
handleLocation('home');
} else {
handleLocation('process');
}
};
export async function init(): Promise<void> {
try {
secureLogger.info('🚀 Starting application initialization...', { component: 'Router' });
// Initialiser uniquement la base de données (sans WebAssembly)
secureLogger.info('🔧 Initializing database...', { component: 'Router' });
const db = await Database.getInstance();
// Register service worker
secureLogger.info('📱 Registering service worker...', { component: 'Router' });
await db.registerServiceWorker('/src/service-workers/database.worker.js');
// Vérifier l'état du storage et naviguer vers la page appropriée
// Cette fonction est maintenant légère et ne nécessite pas WebAssembly
secureLogger.debug('🔍 Checking storage state and navigating to appropriate page...', {
component: 'Router',
});
await checkStorageStateAndNavigate();
secureLogger.info('✅ Application initialization completed successfully', {
component: 'Router',
});
} catch (error) {
secureLogger.error('❌ Application initialization failed:', error, { component: 'Router' });
// Handle WebAssembly memory errors specifically
if (error instanceof RangeError && error.message.includes('WebAssembly.instantiate')) {
secureLogger.error('🚨 WebAssembly memory error detected', { component: 'Router' });
secureLogger.info('💡 Try refreshing the page or closing other tabs to free memory', {
component: 'Router',
});
// Show user-friendly error message
alert('⚠️ Insufficient memory for WebAssembly. Please refresh the page or close other tabs.');
}
secureLogger.info('🔄 Falling back to security-setup...', { component: 'Router' });
await navigate('security-setup');
}
}
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
);
};
// const successResponse = (data: any, origin: string, messageId?: string) => {
// // Not used anymore, response sent directly in handlers
// };
// --- 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: `
<div class="modal-confirmation">
<h3>Liaison avec ${event.origin}</h3>
<p>Vous êtes sur le point de lier l'identité numérique de la clé securisée propre à votre appareil avec ${event.origin}.</p>
<p>Cette action permettra à ${event.origin} d'intéragir avec votre appareil.</p>
<p>Voulez-vous continuer ?</p>
</div>
`,
confirmText: 'Ajouter un service',
cancelText: 'Annuler',
},
true
);
if (!result) {
const errorMsg = 'Failed to pair device: User refused to link';
secureLogger.error(errorMsg, { component: 'Router' });
// Error handling - no response needed
}
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}`;
secureLogger.error(errorMsg, { component: 'Router' });
// Error handling - no response needed
}
};
const handleCreatePairing = async (event: MessageEvent) => {
if (event.data.type !== MessageType.CREATE_PAIRING) {
return;
}
secureLogger.info('📨 [Router] Received CREATE_PAIRING request', { component: 'Router' });
if (services.isPaired()) {
const errorMsg = '⚠️ Device already paired — ignoring CREATE_PAIRING request';
secureLogger.warn(errorMsg, { component: 'Router' });
// Error handling - no response needed
return;
}
try {
const { accessToken } = event.data;
secureLogger.debug('🔐 Checking access token validity...', { component: 'Router' });
const validToken =
accessToken && (await tokenService.validateToken(accessToken, event.origin));
if (!validToken) {
throw new Error('❌ Invalid or expired session token');
}
secureLogger.info('✅ Token validated successfully', { component: 'Router' });
secureLogger.info('🚀 Starting pairing process', { component: 'Router' });
const myAddress = await services.getDeviceAddress();
secureLogger.info('📍 Device address:', { component: 'Router', data: myAddress });
secureLogger.info('🧱 Creating pairing process...', { component: 'Router' });
const createPairingProcessReturn = await services.createPairingProcess('', [myAddress]);
secureLogger.info('🧾 Pairing process created:', {
component: 'Router',
data: createPairingProcessReturn,
});
const pairingId = createPairingProcessReturn.updated_process?.process_id;
const stateId = createPairingProcessReturn.updated_process?.current_process?.states[0]
?.state_id as string;
secureLogger.info('🔗 Pairing ID:', { component: 'Router', data: pairingId });
secureLogger.info('🧩 State ID:', { component: 'Router', data: stateId });
secureLogger.info('🔒 Registering device as paired...', { component: 'Router' });
services.pairDevice(pairingId, [myAddress]);
secureLogger.info('🧠 Handling API return for createPairingProcess...', {
component: 'Router',
});
await services.handleApiReturn(createPairingProcessReturn);
secureLogger.debug('🔍 DEBUG: About to create PRD update...', { component: 'Router' });
secureLogger.info('🧰 Creating PRD update...', { component: 'Router' });
const createPrdUpdateReturn = await services.createPrdUpdate(pairingId, stateId);
secureLogger.info('🧾 PRD update result:', {
component: 'Router',
data: createPrdUpdateReturn,
});
await services.handleApiReturn(createPrdUpdateReturn);
secureLogger.debug('✅ DEBUG: PRD update completed successfully!', { component: 'Router' });
secureLogger.debug('🔍 DEBUG: About to approve change...', { component: 'Router' });
secureLogger.info('✅ Approving change...', { component: 'Router' });
const approveChangeReturn = await services.approveChange(pairingId, stateId);
secureLogger.info('📜 Approve change result:', {
component: 'Router',
data: approveChangeReturn,
});
await services.handleApiReturn(approveChangeReturn);
secureLogger.debug('✅ DEBUG: approveChange completed successfully!', {
component: 'Router',
});
secureLogger.debug(
'🔍 DEBUG: approveChange completed, about to call waitForPairingCommitment...',
{ component: 'Router' }
);
secureLogger.info('⏳ Waiting for pairing process to be committed...', {
component: 'Router',
});
await services.waitForPairingCommitment(pairingId);
secureLogger.debug('✅ DEBUG: waitForPairingCommitment completed successfully!', {
component: 'Router',
});
secureLogger.debug('🔍 DEBUG: About to call confirmPairing...', { component: 'Router' });
secureLogger.info('🔁 Confirming pairing...', { component: 'Router' });
await services.confirmPairing(pairingId);
secureLogger.debug('✅ DEBUG: confirmPairing completed successfully!', {
component: 'Router',
});
secureLogger.info('🎉 Pairing successfully completed!', { component: 'Router' });
// ✅ Send success response to frontend
const successMsg = {
type: MessageType.PAIRING_CREATED,
pairingId,
messageId: event.data.messageId,
};
secureLogger.info('📤 Sending PAIRING_CREATED message to UI:', {
component: 'Router',
data: successMsg,
});
window.parent.postMessage(successMsg, event.origin);
} catch (e) {
const errorMsg = `❌ Failed to create pairing process: ${e}`;
secureLogger.error(errorMsg, { component: 'Router' });
// Error handling - no response needed
}
};
const handleGetMyProcesses = async (event: MessageEvent) => {
if (event.data.type !== MessageType.GET_MY_PROCESSES) {
return;
}
if (!services.isPaired()) {
const errorMsg = 'Device not paired';
secureLogger.warn(errorMsg, { component: 'Router' });
// Error handling - no response needed
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}`;
secureLogger.error(errorMsg, { component: 'Router' });
// Error handling - no response needed
}
};
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';
secureLogger.warn(errorMsg, { component: 'Router' });
// Error handling - no response needed
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}`;
secureLogger.error(errorMsg, { component: 'Router' });
// Error handling - no response needed
}
};
/// 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';
secureLogger.warn(errorMsg, { component: 'Router' });
// Error handling - no response needed
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);
const res: Record<string, any> = {};
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}`;
secureLogger.error(errorMsg, { component: 'Router' });
// Error handling - no response needed
}
};
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}`;
secureLogger.error(errorMsg, { component: 'Router' });
// Error handling - no response needed
}
};
const handleGetPairingId = async (event: MessageEvent) => {
if (event.data.type !== MessageType.GET_PAIRING_ID) {
return;
}
if (!services.isPaired()) {
const errorMsg = 'Device not paired';
secureLogger.warn(errorMsg, { component: 'Router' });
// Error handling - no response needed
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}`;
secureLogger.error(errorMsg, { component: 'Router' });
// Error handling - no response needed
}
};
const handleCreateProcess = async (event: MessageEvent) => {
if (event.data.type !== MessageType.CREATE_PROCESS) {
return;
}
if (!services.isPaired()) {
const errorMsg = 'Device not paired';
secureLogger.warn(errorMsg, { component: 'Router' });
// Error handling - no response needed
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; // Not used
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}`;
secureLogger.error(errorMsg, { component: 'Router' });
// Error handling - no response needed
}
};
const handleNotifyUpdate = async (event: MessageEvent) => {
if (event.data.type !== MessageType.NOTIFY_UPDATE) {
return;
}
if (!services.isPaired()) {
const errorMsg = 'Device not paired';
secureLogger.warn(errorMsg, { component: 'Router' });
// Error handling - no response needed
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}`;
secureLogger.error(errorMsg, { component: 'Router' });
// Error handling - no response needed
}
};
const handleValidateState = async (event: MessageEvent) => {
if (event.data.type !== MessageType.VALIDATE_STATE) {
return;
}
if (!services.isPaired()) {
const errorMsg = 'Device not paired';
secureLogger.warn(errorMsg, { component: 'Router' });
// Error handling - no response needed
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}`;
secureLogger.error(errorMsg, { component: 'Router' });
// Error handling - no response needed
}
};
const handleUpdateProcess = async (event: MessageEvent) => {
if (event.data.type !== MessageType.UPDATE_PROCESS) {
return;
}
if (!services.isPaired()) {
const errorMsg = 'Device not paired';
secureLogger.warn(errorMsg, { component: 'Router' });
// Error handling - no response needed
}
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<string, any> = {};
const publicData: Record<string, any> = {};
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}`;
secureLogger.error(errorMsg, { component: 'Router' });
// Error handling - no response needed
}
};
const handleDecodePublicData = async (event: MessageEvent) => {
if (event.data.type !== MessageType.DECODE_PUBLIC_DATA) {
return;
}
if (!services.isPaired()) {
const errorMsg = 'Device not paired';
secureLogger.warn(errorMsg, { component: 'Router' });
// Error handling - no response needed
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}`;
secureLogger.error(errorMsg, { component: 'Router' });
// Error handling - no response needed
}
};
const handleHashValue = async (event: MessageEvent) => {
if (event.data.type !== MessageType.HASH_VALUE) {
return;
}
secureLogger.info('handleHashValue', { component: 'Router', data: 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}`;
secureLogger.error(errorMsg, { component: 'Router' });
// Error handling - no response needed
}
};
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}`;
secureLogger.error(errorMsg, { component: 'Router' });
// Error handling - no response needed
}
};
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 {
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}`;
secureLogger.error(errorMsg, { component: 'Router' });
// Error handling - no response needed
}
};
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.CREATE_CONVERSATION:
secureLogger.warn('CREATE_CONVERSATION functionality has been removed', {
component: 'Router',
});
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.PAIRING_4WORDS_CREATE:
await handlePairing4WordsCreate(event);
break;
case MessageType.PAIRING_4WORDS_JOIN:
await handlePairing4WordsJoin(event);
break;
case 'LISTENING':
// Parent is listening for messages - no action needed
secureLogger.info('👂 Parent is listening for messages', { component: 'Router' });
break;
case 'IFRAME_READY':
// Iframe is ready - no action needed
secureLogger.info('🔗 Iframe is ready', { component: 'Router' });
break;
default:
secureLogger.warn('Unhandled message type: ${event.data.type}', { component: 'Router' });
}
} catch (error) {
const errorMsg = `Error handling message: ${error}`;
secureLogger.error(errorMsg, { component: 'Router' });
// Error handling - no response needed
}
}
window.parent.postMessage(
{
type: MessageType.LISTENING,
},
'*'
);
}
// 4 Words Pairing Handlers
async function handlePairing4WordsCreate(_event: MessageEvent) {
try {
secureLogger.info('🔐 Handling 4 words pairing create request', { component: 'Router' });
const service = await Services.getInstance();
// Use service variable
secureLogger.info('Service instance:', { component: 'Router', data: service });
const iframePairingService = await import('./services/iframe-pairing.service');
const IframePairingService = iframePairingService.default;
const pairingService = IframePairingService.getInstance();
await pairingService.createPairing();
// Pairing creation initiated - no response needed
} catch (error) {
const errorMsg = `Error creating 4 words pairing: ${error}`;
secureLogger.error(errorMsg, { component: 'Router' });
// Error handling - no response needed
}
}
async function handlePairing4WordsJoin(event: MessageEvent) {
try {
secureLogger.info('🔗 Handling 4 words pairing join request', { component: 'Router' });
const { words } = event.data;
if (!words) {
throw new Error('Words are required for joining pairing');
}
const iframePairingService = await import('./services/iframe-pairing.service');
const IframePairingService = iframePairingService.default;
const pairingService = IframePairingService.getInstance();
await pairingService.joinPairing(words);
// Pairing join initiated - no response needed
} catch (error) {
const errorMsg = `Error joining 4 words pairing: ${error}`;
secureLogger.error(errorMsg, { component: 'Router' });
// Error handling - no response needed
}
}
async function cleanPage() {
const container = document.querySelector('#containerId');
if (container) {
container.innerHTML = '';
}
}
// Essential functions are now handled directly in the application
// No need to import from header component since it was removed
(window as any).navigate = navigate;
// Global function to delete account
(window as any).deleteAccount = async () => {
if (
confirm(
'⚠️ Êtes-vous sûr de vouloir supprimer complètement votre compte ?\n\nCette action est IRRÉVERSIBLE et supprimera :\n• Tous vos processus\n• Toutes vos données\n• Votre wallet\n• Votre historique\n\nTapez "SUPPRIMER" pour confirmer.'
)
) {
const confirmation = prompt('Tapez "SUPPRIMER" pour confirmer la suppression :');
if (confirmation === 'SUPPRIMER') {
try {
const services = await Services.getInstance();
await services.deleteAccount();
// Show success message
alert(
"✅ Compte supprimé avec succès !\n\nLa page va se recharger pour redémarrer l'application."
);
// Reload the page to restart the application
window.location.reload();
} catch (error) {
secureLogger.error('❌ Erreur lors de la suppression du compte:', error, {
component: 'Router',
});
alert('❌ Erreur lors de la suppression du compte. Veuillez réessayer.');
}
} else {
alert('❌ Suppression annulée. Le texte de confirmation ne correspond pas.');
}
}
};
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 || '');
}
}
});
/**
* ÉTAPE 2: Gestion de la sécurité (clés de sécurité)
* Cette étape doit être la première et rien d'autre ne doit s'exécuter en parallèle
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function handleSecurityKeyManagement(): Promise<boolean> {
secureLogger.info('🔐 Starting security key management...', { component: 'Router' });
try {
// Vérifier d'abord si un mode de sécurité est configuré
const { SecurityModeService } = await import('./services/security-mode.service');
const securityModeService = SecurityModeService.getInstance();
const currentMode = await securityModeService.getCurrentMode();
if (!currentMode) {
secureLogger.info('🔐 No security mode configured, redirecting to security setup...', {
component: 'Router',
});
window.location.href = '/src/pages/security-setup/security-setup.html';
return false;
}
secureLogger.info('🔐 Security mode configured:', { component: 'Router', data: currentMode });
// Vérifier si des credentials existent
const { SecureCredentialsService } = await import('./services/secure-credentials.service');
const secureCredentialsService = SecureCredentialsService.getInstance();
const hasCredentials = await secureCredentialsService.hasCredentials();
if (!hasCredentials) {
secureLogger.info('🔐 No security credentials found, redirecting to wallet setup...', {
component: 'Router',
});
window.location.href = '/wallet-setup.html';
return false;
} else {
secureLogger.info('🔐 Security credentials found, verifying access...', {
component: 'Router',
});
// Vérifier l'accès aux credentials
const credentials = await secureCredentialsService.retrieveCredentials('');
if (!credentials) {
secureLogger.error('❌ Failed to access security credentials', { component: 'Router' });
window.location.href = '/wallet-setup.html';
return false;
}
secureLogger.info('✅ Security credentials verified', { component: 'Router' });
return true;
}
} catch (error) {
secureLogger.error('❌ Security key management failed:', error, { component: 'Router' });
secureLogger.info('🔐 Redirecting to security setup...', { component: 'Router' });
window.location.href = '/security-setup.html';
return false;
}
}
/**
* ÉTAPE 5: Handshake
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function performHandshake(_services: any): Promise<void> {
secureLogger.info('🤝 Performing handshake...', { component: 'Router' });
try {
// Le handshake est déjà fait lors de la connexion aux relais
// Cette fonction peut être étendue pour des handshakes supplémentaires
secureLogger.info('✅ Handshake completed', { component: 'Router' });
} catch (error) {
secureLogger.error('❌ Handshake failed:', error, { component: 'Router' });
throw error;
}
}
/**
* ÉTAPE 6: Pairing
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function handlePairing(services: any): Promise<void> {
secureLogger.info('🔗 Handling device pairing...', { component: 'Router' });
try {
// Vérifier le statut de pairing
const isPaired = services.isPaired();
secureLogger.debug('🔍 Device pairing status:', {
component: 'Router',
data: isPaired ? 'Paired' : 'Not paired',
});
if (!isPaired) {
secureLogger.warn('⚠️ Device not paired, user must complete pairing...', {
component: 'Router',
});
// Le pairing sera géré par la page home
return;
} else {
secureLogger.info('✅ Device is already paired', { component: 'Router' });
}
} catch (error) {
secureLogger.error('❌ Pairing handling failed:', error, { component: 'Router' });
throw error;
}
}
/**
* ÉTAPE 7: Écoute des processus
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function startProcessListening(services: any): Promise<void> {
secureLogger.info('👂 Starting process listening...', { component: 'Router' });
try {
// Restore data from database (these operations can fail, so we handle them separately)
try {
secureLogger.info('📊 Restoring processes from database...', { component: 'Router' });
await services.restoreProcessesFromDB();
} catch (error) {
secureLogger.warn('⚠️ Failed to restore processes from database:', {
component: 'Router',
data: error,
});
}
try {
secureLogger.info('🔐 Restoring secrets from database...', { component: 'Router' });
await services.restoreSecretsFromDB();
} catch (error) {
secureLogger.warn('⚠️ Failed to restore secrets from database:', {
component: 'Router',
data: error,
});
}
secureLogger.info('✅ Process listening started', { component: 'Router' });
} catch (error) {
secureLogger.error('❌ Process listening failed:', error, { component: 'Router' });
throw error;
}
}