ncantu c3c11f0ef0 Update userwallet components, services and documentation
**Motivations:**
- Add new features and fixes for userwallet application
- Update documentation for pairing, login state machine, and sync
- Add new utilities for bloom filters, nonce store, and contract versioning
- Fix mempool websocket offline issues

**Root causes:**
- N/A (feature additions and improvements)

**Correctifs:**
- Fix mempool websocket offline handling
- Update ESLint configuration

**Evolutions:**
- Add login state machine service and hook
- Add sync loop service
- Add bloom filter utilities for anti-replay and state visibility
- Add nonce store and contract version utilities
- Update pairing confirmation and graph resolver services
- Add new documentation for features and fixes
- Update userwallet components (LoginScreen, SyncScreen)
- Update types for contract, identity, and messages

**Pages affectées:**
- userwallet/src/components/LoginScreen.tsx
- userwallet/src/components/SyncScreen.tsx
- userwallet/src/hooks/useChannel.ts
- userwallet/src/hooks/useLoginStateMachine.ts (new)
- userwallet/src/services/graphResolver.ts
- userwallet/src/services/pairingConfirm.ts
- userwallet/src/services/syncService.ts
- userwallet/src/services/syncLoop.ts (new)
- userwallet/src/services/loginStateMachine.ts (new)
- userwallet/src/types/contract.ts
- userwallet/src/types/identity.ts
- userwallet/src/types/message.ts
- userwallet/src/utils/canonical.ts
- userwallet/src/utils/identity.ts
- userwallet/src/utils/indexedDbStorage.ts
- userwallet/src/utils/relay.ts
- userwallet/src/utils/verification.ts
- userwallet/src/utils/bloom.ts (new)
- userwallet/src/utils/contractVersion.ts (new)
- userwallet/src/utils/nonceStore.ts (new)
- userwallet/eslint.config.mjs
- userwallet/package.json
- userwallet/package-lock.json
- userwallet/docs/synthese.md
- userwallet/docs/specs-champs-obligatoires-cnil.md (new)
- api-relay/README.md
- features/userwallet-pairing-words-only-finalise.md
- features/userwallet-anti-rejeu-etats-visibles-bloom.md (new)
- features/userwallet-bloom-usage-sync.md (new)
- features/userwallet-contrat-login-reste-a-faire.md (new)
- features/userwallet-ecrans-login-a-valider.md (new)
- features/userwallet-eslint-fix.md (new)
- features/userwallet-login-state-machine.md (new)
- features/userwallet-validation-conformite.md (new)
- fixKnowledge/mempool-websocket-offline-fix.md (new)
- mempool (submodule)
- hash_list.txt
- hash_list_cache.txt
2026-01-26 14:00:32 +01:00

292 lines
6.9 KiB
TypeScript

import type {
Service,
Contrat,
Champ,
Action,
ActionLogin,
Membre,
Pair,
} from '../types/contract';
import type { LoginPath, SignatureRequirement } from '../types/identity';
/**
* Cache local pour les objets du graphe.
*/
interface GraphCache {
services: Map<string, Service>;
contrats: Map<string, Contrat>;
champs: Map<string, Champ>;
actions: Map<string, Action>;
membres: Map<string, Membre>;
pairs: Map<string, Pair>;
}
/**
* Service de résolution du graphe contractuel.
*/
export class GraphResolver {
private cache: GraphCache;
constructor() {
this.cache = {
services: new Map(),
contrats: new Map(),
champs: new Map(),
actions: new Map(),
membres: new Map(),
pairs: new Map(),
};
}
/**
* Add a service to the cache.
*/
addService(service: Service): void {
this.cache.services.set(service.uuid, service);
}
/**
* Add a contrat to the cache.
*/
addContrat(contrat: Contrat): void {
this.cache.contrats.set(contrat.uuid, contrat);
}
/**
* Add a champ to the cache.
*/
addChamp(champ: Champ): void {
this.cache.champs.set(champ.uuid, champ);
}
/**
* Add an action to the cache.
*/
addAction(action: Action): void {
this.cache.actions.set(action.uuid, action);
}
/**
* Add a membre to the cache.
*/
addMembre(membre: Membre): void {
this.cache.membres.set(membre.uuid, membre);
}
/**
* Add a pair to the cache.
*/
addPair(pair: Pair): void {
this.cache.pairs.set(pair.uuid, pair);
}
/**
* Get all services from cache.
*/
getServices(): Service[] {
return Array.from(this.cache.services.values());
}
/**
* Get all membres from cache.
*/
getMembres(): Membre[] {
return Array.from(this.cache.membres.values());
}
/**
* Get all contrats from cache.
*/
getContrats(): Contrat[] {
return Array.from(this.cache.contrats.values());
}
/**
* Get all pairs from cache.
*/
getPairs(): Pair[] {
return Array.from(this.cache.pairs.values());
}
/**
* Get all actions from cache.
*/
getActions(): Action[] {
return Array.from(this.cache.actions.values());
}
/**
* Resolve login path: Service → Contrat → Champ → ActionLogin → Membre → Pair.
*/
resolveLoginPath(
serviceUuid: string,
membreUuid: string,
): LoginPath | null {
const service = this.cache.services.get(serviceUuid);
if (service === undefined) {
return null;
}
const contrat = this.cache.contrats.get(service.contrat_uuid);
if (contrat === undefined) {
return {
service_uuid: serviceUuid,
contrat_uuid: [],
action_login_uuid: '',
membre_uuid: membreUuid,
pairs_attendus: [],
signatures_requises: [],
statut: 'incomplet',
};
}
const contratVersion = contrat.version;
const champs = Array.from(this.cache.champs.values()).filter((c) =>
c.contrats_parents_uuid.includes(service.contrat_uuid),
);
const membre = this.cache.membres.get(membreUuid);
if (membre === undefined) {
return {
service_uuid: serviceUuid,
contrat_uuid: [service.contrat_uuid],
contrat_version: contratVersion,
champ_uuid: champs.map((c) => c.uuid),
action_login_uuid: '',
membre_uuid: membreUuid,
pairs_attendus: [],
signatures_requises: [],
statut: 'incomplet',
};
}
const actionLogin = this.findActionLogin(membre.actions_parents_uuid);
if (actionLogin === null) {
return {
service_uuid: serviceUuid,
contrat_uuid: [service.contrat_uuid],
contrat_version: contratVersion,
champ_uuid: champs.map((c) => c.uuid),
action_login_uuid: '',
membre_uuid: membreUuid,
pairs_attendus: [],
signatures_requises: [],
statut: 'incomplet',
};
}
const pairsAttendus = Array.from(this.cache.pairs.values())
.filter((p) => p.membres_parents_uuid.includes(membreUuid))
.map((p) => p.uuid);
const signaturesRequises = this.computeRequiredSignatures(
actionLogin,
membreUuid,
);
const statut =
pairsAttendus.length > 0 && signaturesRequises.length > 0
? 'complet'
: 'incomplet';
return {
service_uuid: serviceUuid,
contrat_uuid: [service.contrat_uuid],
contrat_version: contratVersion,
champ_uuid: champs.map((c) => c.uuid),
action_login_uuid: actionLogin.uuid,
membre_uuid: membreUuid,
pairs_attendus: pairsAttendus,
signatures_requises: signaturesRequises,
statut,
};
}
/**
* Find action login from action UUIDs.
*/
private findActionLogin(actionUuids: string[]): ActionLogin | null {
for (const uuid of actionUuids) {
const action = this.cache.actions.get(uuid);
if (action !== undefined && this.isActionLogin(action)) {
return action as ActionLogin;
}
}
return null;
}
/**
* Check if an action is a login action.
*/
private isActionLogin(action: Action): boolean {
return (
action.types.types_names_chiffres.includes('login') ||
action.types.types_uuid.some((uuid) => uuid.includes('login'))
);
}
/**
* Compute required signatures from validators.
*/
private computeRequiredSignatures(
action: Action,
membreUuid: string,
): SignatureRequirement[] {
const requirements: SignatureRequirement[] = [];
for (const membreRole of action.validateurs_action.membres_du_role) {
if (membreRole.membre_uuid === membreUuid) {
for (const sigReq of membreRole.signatures_obligatoires) {
requirements.push({
membre_uuid: sigReq.membre_uuid,
pair_uuid: undefined,
cle_publique: sigReq.cle_publique,
cardinalite_minimale: sigReq.cardinalite_minimale,
});
}
}
}
return requirements;
}
/**
* Validate that all required parents exist (at least 1 constraint).
*/
validateParentsConstraints(): {
valid: boolean;
errors: string[];
} {
const errors: string[] = [];
for (const champ of this.cache.champs.values()) {
if (champ.contrats_parents_uuid.length === 0) {
errors.push(`Champ ${champ.uuid} has no parent contrat`);
}
}
for (const action of this.cache.actions.values()) {
if (action.contrats_parents_uuid.length === 0) {
errors.push(`Action ${action.uuid} has no parent contrat`);
}
}
for (const membre of this.cache.membres.values()) {
if (membre.actions_parents_uuid.length === 0) {
errors.push(`Membre ${membre.uuid} has no parent action`);
}
}
for (const pair of this.cache.pairs.values()) {
if (pair.membres_parents_uuid.length === 0) {
errors.push(`Pair ${pair.uuid} has no parent membre`);
}
}
return {
valid: errors.length === 0,
errors,
};
}
}