Files
khadhroony-solana-project/crates/ksp-config-lib/USAGE.md

26 KiB
Raw Blame History

Utilisation de ksp-config-lib

1. Bootstrap et moteur documentaire

Config doit interpréter ses propres arguments de bootstrap avant toute lecture de document :

let args: std::vec::Vec<std::ffi::OsString> = 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 :

--cfgpath=/path/to/config
--schemapath=/path/to/schemas
--filemap=cfg.composite.ksp-app-raw-transaction-ingest-desk=my-raw-ingest-desk.json
--filemap=cfg.composite.ksp-app-solprices-desk=my-solprices-desk.json
--filemap=cfg.composite.ksp-app-wallet-desk=my-wallet-desk.json
--filemap=cfg.std.logging=my-logging.json
--filemap=cfg.std.offchain_transport=my-offchain-transport.json
--filemap=cfg.std.store=my-store.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 :

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 :

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 :

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 :

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 :

${KSP_LOGS_DIRECTORY}
${KSP_LOGS_DIRECTORY:-logs}

Pour conserver la sensibilité et la provenance, préférer les variantes détaillées :

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 :

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 + Yellowstone gRPC depuis Config

Config possède l'adapter du document std.transport vers les contrats runtime de ksp-onchain-transport-lib :

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 grpc_settings = transport.grpc_settings();
let _ = (http_settings, ws_settings, grpc_settings);

std.transport V3 conserve intégralement les shapes V1/V2, ajoute grpc_defaults au niveau global et permet profiles[].grpc_endpoints[]. Les profils V3 peuvent rester HTTP + WebSocket seulement : l'absence de grpc_endpoints mappe vers None et n'invente aucun endpoint. La lecture V1 HTTP-only et V2 HTTP + WebSocket reste stricte et backward-compatible.

Un endpoint gRPC V3 sépare explicitement :

provider = description de l'opérateur/exécution, par exemple publicnode
protocol = solana_yellowstone
metadata = metadata non secrète
secret_metadata = metadata dont la valeur doit avoir une provenance KSP_SECRET_*/KSPB_SECRET_*

protocol n'est pas un nouveau WsProtocolKind et ne transforme pas PublicNode en protocole. Config résout les placeholders, vérifie la classe de sensibilité des metadata puis construit YellowstoneGrpcTransportSettings. Transport ne lit jamais l'environnement. Les valeurs de secret_metadata sont disponibles au runtime mais redacted dans safe_value et dans les représentations Debug.

Les profils versionnés publicnode_mainnet et publicnode_testnet représentent leurs endpoints Yellowstone sous la forme URL TLS requise par Transport et conservent le x-token dans secret_metadata. Ils restent des sources Transport network-scoped et ne définissent jamais lidentité du réseau applicatif.

L'accesseur historique into_transport_settings() conserve volontairement son tuple (HTTP, Option<WS>). Un consumer ayant besoin des trois backends utilise into_all_transport_settings() ou les accesseurs séparés afin de ne pas casser silencieusement les consumers V2.

Les protocoles WebSocket restent kind = "solana_standard" et kind = "helius_laserstream". Chaque endpoint peut en plus déclarer capabilities, liste de descriptors WsSubscriptionKind (account, block, logs, program, root, signature, slot, slots_updates, vote, helius_transaction). L'absence de cette liste reste backward-compatible mais ne constitue aucune preuve de support ; une liste présente est mappée explicitement vers Transport puis validée contre le protocole. Config ne déduit jamais une capability depuis provider.

Les profils Solana standard versionnés déclarent conservativement les six familles stables account/logs/program/root/signature/slot et ne déclarent donc pas block. Le profil testnet_public fournit le socle HTTP + WS standard Testnet sans credential ; publicnode_testnet reste une source Yellowstone optionnelle séparée. Les profils helius_devnet et helius_mainnet déclarent les sept familles Helius déjà supportées par la façade (account/logs/program/root/signature/slot/slots_updates) plus helius_transaction, sans block ni vote. Helius LaserStream WebSocket conserve ses URLs Config-owned et sa clé KSP_SECRET_HELIUS_API_KEY; cette surface est indépendante de Yellowstone gRPC.

Les scalaires *_ms restent des valeurs Config et sont convertis en std::time::Duration par l'adapter. 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 :

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 naccepte 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.

4.3 Construire le service Off-chain Transport depuis Config

Le document cfg.std.offchain_transport possède actuellement le domaine market_price. Config résout les placeholders, vérifie la provenance des API keys et de la paire DexScreener, puis construit directement MarketPriceService :

let offchain = match engine.load_resolved_offchain_transport_config(
    std::option::Option::None,
    &environment,
) {
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => return std::result::Result::Err(error),
};

for entry in offchain.service().registry().entries() {
    println!(
        "{} {:?}",
        entry.descriptor().display_name(),
        entry.state().availability(),
    );
}

Le profil versionné public_keyless ne requiert aucun secret. Le profil all_free utilise les credentials KSP_SECRET_* et la paire KSP_PUBLIC_DEXSCREENER_SOL_USD_PAIR_ADDRESS inventoriés dans .env.example. Une API key littérale ou issue d'une provenance non secrète est refusée par l'adapter effectif ; une paire DexScreener issue d'une provenance Secret est également refusée.

Config ne permet pas de fournir base_url, endpoint_url, rate_limit ou requests_per aux branches provider. Les origines et cadences sûres restent possédées par ksp-offchain-transport-lib. La direction de dépendance reste donc :

ksp-config-lib -> ksp-offchain-transport-lib

Off-chain Transport ne lit ni .env, ni KSP_*, ni les documents Config. ksp-app-solprices-desk reçoit le service déjà composé puis utilise uniquement la surface provider-neutral registry(), refresh, refresh_many et refresh_all.

4.4 Construire le Store depuis Config

cfg.std.store définit des targets nommés. Chaque target sélectionne exactement un réseau logique, un backend et une URI PostgreSQL distincte. Config résout les secrets puis construit le contrat backend-neutral ksp_store_lib::StoreSettings sans activer la feature PostgreSQL du consumer :

let store_config = match engine.load_resolved_store_config(
    std::option::Option::Some("devnet"),
    &environment,
) {
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => return std::result::Result::Err(error),
};

let target_id = store_config.target_id();
let network = store_config.settings().network();
let _ = (target_id, network);

let store_settings = store_config.into_settings();
let store = match ksp_store_lib::Store::open(store_settings).await {
    std::result::Result::Ok(value) => value,
    std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let _health = store.health().await;
let closed = store.close().await;
if let std::result::Result::Err(error) = closed {
    return std::result::Result::Err(error);
}

Targets committed :

devnet   -> network devnet       -> KSP_SECRET_STORE_DEVNET_POSTGRES_URI
mainnet  -> network mainnet -> KSP_SECRET_STORE_MAINNET_POSTGRES_URI
testnet  -> network testnet      -> KSP_SECRET_STORE_TESTNET_POSTGRES_URI

default_profile = "devnet" choisit un seul target. La sélection d'un autre target se fait par le profile_id explicite ; ksp-store-lib ne multiplexe pas plusieurs bases ou réseaux dans une même instance.

Chaque connection_uri doit provenir d'un placeholder KSP_SECRET_*/KSPB_SECRET_*. Une URI littérale ou issue d'une variable non secrète est rejetée par l'adapter effectif. La valeur réelle est transmise au runtime Store, mais ResolvedStoreConfig, StoreSettings et les projections sûres ne l'affichent pas.

La direction de dépendance reste :

ksp-config-lib -> ksp-store-lib (default-features = false)
ksp-store-lib -X-> ksp-config-lib
ksp-store-postgres-lib -X-> ksp-config-lib

5. Profils et composites

Pour un document standard profilé :

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ë.

Lorsquun adapter runtime consomme un profil déjà choisi par un composite, utiliser lentrée dédiée afin de préserver ConfigProfileSelectionSource::Composite :

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 cfg.composite.ksp-app-raw-transaction-ingest-desk référence exactement logging, transport et store; ses profils couplent explicitement un profil Transport et un profil Store du même réseau logique (devnet, mainnet ou testnet) afin que l'application n'assemble jamais arbitrairement deux réseaux. Le composite cfg.composite.ksp-app-solprices-desk référence logging et offchain_transport. Le composite cfg.composite.ksp-app-wallet-desk référence logging, offchain_transport, transport et wallet; chaque application valide ses frontières de composition au bootstrap.

6. Management de std.logging.json

Une application de management construit la façade à partir d'un moteur :

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 :

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 :

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 :

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 :

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.

Le composite cfg.composite.ksp-app-raw-transaction-ingest-desk expose des profils applicatifs devnet, mainnet et testnet. Un même profil peut référencer transport, transport.helius et transport.yellowstone tant que tous les profils Transport résolus appartiennent au même réseau que Store. Le provider identifie une source de capability, jamais un réseau logique.

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 §12
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, §78
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 §67
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 :

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

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 :

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

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 :

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.