# Utilisation de ksp-config-lib ## 1. Bootstrap et moteur documentaire Config doit interpréter ses propres arguments de bootstrap avant toute lecture de document : ```rust let args: std::vec::Vec = std::env::args_os().collect(); let bootstrap = match ksp_config_lib::ConfigBootstrapOptions::from_args(args.as_slice()) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let registry = match ksp_config_lib::ConfigFileRegistry::from_args(args.as_slice()) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let engine = ksp_config_lib::ConfigDocumentEngine::new(bootstrap, registry); ``` Les arguments compris par Config sont : ```text --cfgpath=/path/to/config --schemapath=/path/to/schemas --filemap=cfg.composite.ksp-app-wallet-desk=my-wallet-desk.json --filemap=cfg.std.logging=my-logging.json --filemap=cfg.std.transport=my-transport.json --filemap=cfg.std.wallet=my-wallet.json ``` `cfgpath` et `schemapath` ne sont jamais lus depuis JSON, `.env` ou une variable KSP : cette règle évite un bootstrap récursif. ### 1.1 Inventorier les fichiers enregistrés Le registre expose une vue read-only déterministe des descripteurs connus : ```rust for descriptor in registry.descriptors() { let file_id = descriptor.file_id().as_str(); let kind = descriptor.kind(); let filename = descriptor.filename(); let schema_file_id = descriptor.schema_file_id(); let _ = (file_id, kind, filename, schema_file_id); } ``` L'ordre est celui des `file_id`. La vue reflète les éventuels overrides `--filemap` déjà appliqués tout en conservant le kind et l'association de schema. Elle permet notamment à une application de management de construire sa liste de documents/schemas sans dupliquer le registre dans sa propre couche. ## 2. Charger et valider un document connu Les consumers utilisent un `file_id` logique : ```rust let file_id = match ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_STD_LOGGING) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let document = engine.load_validated_document(&file_id); ``` Le moteur résout le path physique via le registre, charge le schema associé, valide le schema lui-même, valide l'instance puis applique les invariants sémantiques KSP. ## 3. Environnement effectif `ConfigEnvironment::load()` capture les variables process KSP/KSPB et lit `./.env` : ```rust let environment = match ksp_config_lib::ConfigEnvironment::load() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; ``` La priorité est : ```text process environment > .env > fallback > missing ``` Une chaîne vide explicitement présente est une valeur définie ; elle ne provoque pas l'utilisation du fallback. Exemples de placeholders : ```text ${KSP_LOGS_DIRECTORY} ${KSP_LOGS_DIRECTORY:-logs} ``` Pour conserver la sensibilité et la provenance, préférer les variantes détaillées : ```rust let resolved = environment.resolve_text_detailed("${KSP_SECRET_EXAMPLE}"); ``` `ResolvedConfigText` / `ResolvedConfigJson` séparent valeur réelle et valeur sûre. Une représentation `Debug` ne doit pas révéler le réel d'un secret. ## 4. Construire Logging depuis Config Le chemin normal consiste à charger le profil Logging, résoudre l'environnement puis construire directement le contrat Logging public : ```rust let resolved = match engine.load_resolved_logging_config(std::option::Option::None, &environment) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let settings = resolved.into_settings(); let initialized = ksp_logging_lib::initialize(&settings); let mut logging_guard = match initialized { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; ``` L'application/service possède `logging_guard`. Config ne conserve pas de singleton Logging. Un `logs_directory` relatif est ancré sur le current working directory du processus. Un path absolu est conservé. Une valeur explicite invalide produit une erreur effective : elle ne retombe pas silencieusement sur le fallback du placeholder. Les `files[].path` restent relatifs sous le root Logging, y compris après interpolation. ### 4.1 Construire le Transport HTTP + WebSocket depuis Config Config possède également l'adapter du document `std.transport` vers le contrat runtime de `ksp-onchain-transport-lib` : ```rust let transport = match engine.load_resolved_transport_config(std::option::Option::None, &environment) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let http_settings = transport.http_settings(); let ws_settings = transport.ws_settings(); let _ = (http_settings, ws_settings); ``` `std.transport` V2 conserve `retry` et `profiles[].endpoints[]` pour HTTP, ajoute `ws_defaults` et `profiles[].ws_endpoints[]`, puis accepte les protocoles WebSocket `kind = "solana_standard"` et `kind = "helius_laserstream"`. Ce second discriminateur appartient exclusivement au namespace WebSocket et mappe vers `WsProtocolKind::HeliusLaserStream`; il ne préfigure aucun contrat LaserStream gRPC. Un `ws_endpoints[].session` optionnel surcharge seulement les paramètres génériques de `WsSessionSettings`. Pour Helius LaserStream WebSocket, l'exemple versionné couvre explicitement les deux réseaux supportés par ce contrat : mainnet via `wss://mainnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-replace-me}` et devnet via `wss://devnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-replace-me}`. Les deux réseaux vivent dans des profils Config distincts afin de ne pas mélanger des clusters dans un même profil logique. Config reste l'unique propriétaire de `KSP_SECRET_HELIUS_API_KEY` : il résout la clé dans l'URL effective et transmet au Transport un `WsEndpointUrl` utilisable au runtime. Dans `safe_value`, Config conserve les segments littéraux non sensibles d'une chaîne composée et remplace uniquement chaque segment secret par `********` ; les projections deviennent donc respectivement `wss://mainnet.helius-rpc.com/?api-key=********` et `wss://devnet.helius-rpc.com/?api-key=********`. Les représentations `Debug` restent sûres et n'exposent jamais la clé réelle. Transport ne lit jamais directement l'environnement. Le même schema enregistré conserve la lecture stricte du V1 historique : dans ce cas `http_settings()` reste disponible et `ws_settings()` retourne `None`. Aucun `WsTransportSettings` vide n'est inventé pour simuler l'absence de WebSocket. Les scalaires `*_ms` restent des valeurs Config et sont convertis en `std::time::Duration` par l'adapter. Les URLs HTTP et WebSocket peuvent provenir de `KSP_PUBLIC_*` ou de `KSP_SECRET_*`; dans ce dernier cas la valeur réelle reste disponible au runtime Transport, mais `ResolvedTransportConfig::effective().safe_value()` et les représentations `Debug` sont redacted. La dépendance reste unidirectionnelle : Config connaît les contrats Transport pour les construire ; Transport ne connaît ni Config, ni `.env`, ni les variables KSP. ### 4.2 Résoudre le répertoire Wallet depuis Config `std.wallet` conserve une racine globale et laisse un profil ajouter un sous-répertoire relatif : ```rust let wallet = match engine.load_resolved_wallet_config(std::option::Option::None, &environment) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let root = wallet.wallets_directory(); let profile_subdirectory = wallet.wallets_subdirectory(); let effective = wallet.effective_wallets_directory(); let _ = (root, profile_subdirectory, effective); ``` `wallets_directory` accepte `${KSP_WALLETS_DIRECTORY:-wallets}` et une valeur relative est ancrée au current working directory du processus. `wallets_subdirectory` n’accepte que des composants relatifs normaux : chemin absolu, `.` et `..` sont rejetés. Config résout/valide le chemin mais **ne crée pas** les répertoires et n’énumère aucun `.kspwallet`; ces responsabilités appartiennent au consumer applicatif. Les chemins Wallet refusent toute valeur `KSP_SECRET_*`. Les futurs `KSP_SECRET_WALLET_PASS_*` constituent un flux de secrets distinct et ne sont pas des champs de `std.wallet.json`. ## 5. Profils et composites Pour un document standard profilé : ```rust let profile = engine.load_resolved_profile(&file_id, std::option::Option::None); ``` `None` utilise le `default_profile`; `Some("profile_id")` impose un profil explicite. Un composite référence les documents par `file_id`, jamais par filename. `load_resolved_composite(...)` conserve chaque `ResolvedConfigProfile` composant et sa provenance plutôt que d'aplatir plusieurs domaines dans une map ambiguë. Lorsqu’un adapter runtime consomme un profil déjà choisi par un composite, utiliser l’entrée dédiée afin de préserver `ConfigProfileSelectionSource::Composite` : ```rust let component = match composite.component("wallet") { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(/* erreur applicative */), }; let wallet = engine.resolve_wallet_config_profile(component.resolved(), &environment); ``` La même forme existe pour Logging via `resolve_logging_config_profile`. Le composite concret `cfg.composite.ksp-app-wallet-desk` référence actuellement `logging`, `transport` et `wallet`; Wallet Desk valide ces trois frontières au bootstrap. ## 6. Management de `std.logging.json` Une application de management construit la façade à partir d'un moteur : ```rust let management = ksp_config_lib::ConfigManagement::new(engine); let document = match management.load_logging_document() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; ``` Le type `LoggingConfigDocument` et ses sous-structures exposent des setters/mutators typés. Après modification, la sauvegarde : ```rust let saved = management.save_logging_document(&document); ``` valide le candidat complet avant toute substitution du fichier. Un candidat invalide ne remplace pas la source existante. `read_source(file_id)` reste disponible pour une UI de réparation : il peut lire le texte brut d'un document enregistré même lorsque son JSON ou son schema est invalide. Il n'ouvre pas un path arbitraire. ## 7. Management de `.env` Les rapports ordinaires sont sûrs : ```rust let report = management.environment_report(); ``` Ils distinguent notamment valeur souhaitée `.env`, valeur effective, source et shadowing process sans exposer un secret réel. L'accès au réel est volontairement explicite : ```rust let effective = management.reveal_effective_environment_value("KSP_SECRET_EXAMPLE"); let persisted = management.reveal_dotenv_value("KSP_SECRET_EXAMPLE"); ``` Une application doit contrôler l'autorisation de l'utilisateur avant ces appels et ne jamais journaliser les valeurs retournées. Les mutations persistantes utilisent : ```rust let changed = management.set_dotenv_value("KSP_LOGS_DIRECTORY", "logs"); let removed = management.remove_dotenv_value("KSP_LOGS_DIRECTORY"); ``` Elles n'altèrent jamais l'environnement hérité du processus. Une valeur process peut donc masquer une modification `.env`; `ConfigEnvironmentChangeReport` distingue `source_changed`, `effective_changed`, `shadowed_by_process_environment` et `reload_required`. Sur Unix, un nouveau `.env` est créé avec des permissions privées `0600`; les permissions existantes sont préservées lors des remplacements atomiques. ## 8. `.env.example` `/.env.example` est l'inventaire versionné. `/.env` reste local et ignoré. Toute nouvelle variable runtime concrète `KSP_*` / `KSPB_*` introduite dans le code ou les documents Config doit être ajoutée à `.env.example` avec un commentaire expliquant son usage. Les audits `ksp-config-lib/tests/ownership.rs` font échouer `cargo test` lorsqu'une clé concrète est oubliée. ## 9. Frontière Tauri Une application Tauri doit appeler les APIs ci-dessus via ses commandes/DTO applicatifs. Elle ne lit ni JSON ni `.env` directement et ne résout jamais elle-même les placeholders. Les valeurs `Secret` ne doivent pas être incluses par défaut dans les DTO publics. Une action UI explicitement autorisée peut appeler une méthode `reveal_*` et transporter le résultat par un DTO spécifique, sans log ni diagnostic contenant la valeur réelle. ## 10. Index de la surface publique Ce guide reste volontairement indépendant des numéros de release. Les contrats publics sont regroupés ci-dessous par usage ; les constantes de noms/erreurs accompagnent les mêmes familles et ne constituent pas des workflows séparés. | Famille publique | Contrats principaux | Exemple | |------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------| | Bootstrap | `ConfigBootstrapOptions`, `ARG_CFG_PATH`, `ARG_SCHEMA_PATH`, `DEFAULT_CFG_PATH`, `DEFAULT_SCHEMA_PATH` | §1 | | Registre logique | `ConfigFileRegistry`, `ConfigFileId`, `ConfigFileDescriptor`, `ConfigFileKind`, `ARG_FILE_MAP`, constantes `FILE_ID_*` / `DEFAULT_*_FILENAME` | §1–2 | | Documents | `ConfigDocumentEngine`, `ConfigJsonDocument` | §2 | | Profils | `ResolvedConfigProfile`, `ConfigProfileSelectionSource`, `ConfigValueOrigin` | §5 | | Composites | `ResolvedConfigComposite`, `ResolvedCompositeComponent` | §5 | | Environnement | `ConfigEnvironment`, `ConfigEnvironmentSource`, `ConfigEnvironmentValue`, `DEFAULT_DOTENV_PATH`, `DEFAULT_DOTENV_EXAMPLE_PATH` | §3, §7–8 | | Sensibilité/provenance | `ConfigSensitivity`, `ConfigValueProvenance`, `ResolvedConfigText`, `ResolvedConfigJson`, `REDACTED_CONFIG_VALUE` | §3 | | Logging effectif | `ResolvedLoggingConfig` | §4 | | Transport effectif | `ResolvedTransportConfig` | §4.1 | | Management | `ConfigManagement`, `ConfigManagedSource`, `ConfigDocumentChangeReport`, `ConfigEnvironmentReport`, `ConfigEnvironmentChangeReport` | §6–7 | | Source Logging typée | `LoggingConfigDocument`, `LoggingProfileConfig`, `LoggingConsoleConfig`, `LoggingFileConfig`, `LoggingOutputFilterConfig`, `LoggingTargetFilterConfig` | §6 et exemple ci-dessous | | Erreurs Config | constantes `ERROR_CODE_*` réexportées par la crate | exemple ci-dessous | ### 10.1 Modifier une configuration Logging typée Les getters permettent d'inspecter la source ; les setters et vues `*_mut()` permettent de construire un candidat avant validation/persistence : ```rust let mut document = match management.load_logging_document() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; document.set_logs_directory("${KSP_LOGS_DIRECTORY:-logs}"); if let std::option::Option::Some(profile) = document.profiles_mut().first_mut() { profile.set_default_filter("debug"); profile.console_mut().set_enabled(true); profile.console_mut().filter_mut().set_level("info"); profile.console_mut().filter_mut().domains_mut().push("config".to_owned()); } let saved = match management.save_logging_document(&document) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if saved.source_changed() && saved.reload_required() { // The application decides when/how to reload the affected runtime consumer. } ``` La construction depuis zéro utilise les constructeurs publics `LoggingConfigDocument::new`, `LoggingProfileConfig::new`, `LoggingConsoleConfig::new`, `LoggingFileConfig::new`, `LoggingOutputFilterConfig::new` et `LoggingTargetFilterConfig::new`. Les mêmes contraintes schema/sémantiques sont appliquées au moment de `save_logging_document()`. ### 10.2 Inspecter et réparer un source enregistré sans contourner Config ```rust let source = match management.read_source(&file_id) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let logical_id = source.file_id(); let managed_path = source.path(); let raw_content = source.content(); ``` Cette lecture est notamment destinée à une UI de réparation lorsque le document n'est plus validable. Elle n'autorise pas la lecture d'un chemin arbitraire. Après édition du texte brut, le candidat est soumis à Config : ```rust let saved = match management.save_source_candidate(&file_id, edited_source.as_str()) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if saved.source_changed() && saved.reload_required() { // Reload the affected Config consumer through the application lifecycle. } ``` `save_source_candidate()` : - accepte uniquement un `file_id` enregistré de kind `Config` ; - parse le candidat comme JSON ; - valide le schema enregistré et les invariants sémantiques KSP ; - ne remplace aucune donnée lorsque l'une de ces validations échoue ; - persiste atomiquement le texte brut validé sans reformattage implicite ; - retourne `ConfigDocumentChangeReport` pour distinguer un changement réel d'un candidat identique. Le path reste résolu exclusivement par `ConfigFileRegistry`; l'appelant ne fournit jamais de path physique. ### 10.3 Exploiter les rapports `.env` ```rust let reports = match management.environment_report() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; for report in reports { let name = report.variable_name(); let sensitivity = report.sensitivity(); let desired = report.desired_safe_value(); let effective = report.effective_safe_value(); let source = report.effective_source(); let shadowed = report.shadowed_by_process_environment(); let _ = (name, sensitivity, desired, effective, source, shadowed); } ``` Après une mutation, `ConfigEnvironmentChangeReport` expose `source_changed()`, `effective_changed()`, `shadowed_by_process_environment()` et `reload_required()`. ### 10.4 Distinguer un code d'erreur Config Les codes publics permettent à une UI/service de brancher sa logique sans parser le texte du message : ```rust let loaded = engine.load_validated_document(&file_id); if let std::result::Result::Err(error) = loaded { if error.code() == ksp_config_lib::ERROR_CODE_SCHEMA_VALIDATION_FAILED { // Present a schema-specific diagnostic path to the caller. } return std::result::Result::Err(error); } ``` Le message/context d'erreur reste destiné au diagnostic ; l'identité machine-readable passe par `ErrorCode`.