**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
292 lines
6.9 KiB
TypeScript
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,
|
|
};
|
|
}
|
|
}
|