v0.2.6-pre.003

This commit is contained in:
2026-08-20 17:08:32 +02:00
parent 1a57cdeef0
commit f9f9ac79b6
40 changed files with 1735 additions and 122 deletions

View File

@@ -5,6 +5,14 @@
# The current Config document fallback is "logs" when neither the process environment nor .env defines this variable. # The current Config document fallback is "logs" when neither the process environment nor .env defines this variable.
KSP_LOGS_DIRECTORY=logs KSP_LOGS_DIRECTORY=logs
# KSP Wallet root directory. Used by config/std.wallet.json before an optional profile subdirectory is appended.
# The committed Wallet document falls back to "wallets" when neither the process environment nor .env defines this variable.
KSP_WALLETS_DIRECTORY=wallets
# Optional Wallet Desk password candidates are introduced by the later unlock tranche.
# Additional KSP_SECRET_WALLET_PASS_<LABEL> variables may be defined locally; never commit real password values.
# KSP_SECRET_WALLET_PASS_01=
# Optional public Solana Devnet HTTP endpoint override used by config/std.transport.json. # Optional public Solana Devnet HTTP endpoint override used by config/std.transport.json.
# The committed Transport document falls back to https://api.devnet.solana.com when this variable is absent. # The committed Transport document falls back to https://api.devnet.solana.com when this variable is absent.
KSP_PUBLIC_SOLANA_DEVNET_HTTP_URL=https://api.devnet.solana.com KSP_PUBLIC_SOLANA_DEVNET_HTTP_URL=https://api.devnet.solana.com

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml # file: Cargo.toml
# version: 165 # version: 166
[workspace] [workspace]
resolver = "3" resolver = "3"
members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"] members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"]
[workspace.package] [workspace.package]
version = "0.2.6-pre.2" version = "0.2.6-pre.3"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -1,5 +1,5 @@
<!-- file: ROADMAP.md --> <!-- file: ROADMAP.md -->
<!-- version: 58 --> <!-- version: 59 -->
# Roadmap KSP # Roadmap KSP
@@ -50,7 +50,7 @@ Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues. U
- [X] `0.2.3` — HTTP Transactions stable : 11/11 wrappers typés publiés, classification `8 Read / 2 WriteSubmission / 1 Simulation`, no-resend ambigu prouvé pour les write submissions, `KSP-TRANSPORT-007` réaudité conforme sur les 37 wrappers HTTP courants, graphes Cargo et deux smokes Devnet validés ; `0.2.4` reprend les 15 Blocks/Economics restants. - [X] `0.2.3` — HTTP Transactions stable : 11/11 wrappers typés publiés, classification `8 Read / 2 WriteSubmission / 1 Simulation`, no-resend ambigu prouvé pour les write submissions, `KSP-TRANSPORT-007` réaudité conforme sur les 37 wrappers HTTP courants, graphes Cargo et deux smokes Devnet validés ; `0.2.4` reprend les 15 Blocks/Economics restants.
- [X] `0.2.4` — HTTP Blocks + Economics stable : 15/15 wrappers `V0_2_4` publiés, surface typed complète à 52/52 méthodes courantes, 14/14 historiques conservées, réaudit SIMD/inventaire final et `KSP-TRANSPORT-007` global validés ; deux smokes Devnet passés avant publication. - [X] `0.2.4` — HTTP Blocks + Economics stable : 15/15 wrappers `V0_2_4` publiés, surface typed complète à 52/52 méthodes courantes, 14/14 historiques conservées, réaudit SIMD/inventaire final et `KSP-TRANSPORT-007` global validés ; deux smokes Devnet passés avant publication.
- [X] `0.2.5` — Wallet foundation stable : `.kspwallet` V1, VIEW/OWNER indépendants, Argon2id/XChaCha20-Poly1305, autorité Ed25519 OWNER, persistence no-clobber, signature, administration/rotations/révocation VIEW forte, import/export Solana CLI JSON + Base58, canaris adversariaux, interop externe et documentation durable publiés. La clôture `pre.010-fix.001``fix.003` ajoute `ed25519-dalek 3.0.0` direct, normalise le Rust workspace et installe laudit structurel Python complémentaire à rustfmt/Clippy. `Pubkey` reste via `ksp-core-lib`, la keypair reste encapsulée dans Wallet et Config/Transport/ExecutionPolicy/Store/Tauri restent hors Wallet. - [X] `0.2.5` — Wallet foundation stable : `.kspwallet` V1, VIEW/OWNER indépendants, Argon2id/XChaCha20-Poly1305, autorité Ed25519 OWNER, persistence no-clobber, signature, administration/rotations/révocation VIEW forte, import/export Solana CLI JSON + Base58, canaris adversariaux, interop externe et documentation durable publiés. La clôture `pre.010-fix.001``fix.003` ajoute `ed25519-dalek 3.0.0` direct, normalise le Rust workspace et installe laudit structurel Python complémentaire à rustfmt/Clippy. `Pubkey` reste via `ksp-core-lib`, la keypair reste encapsulée dans Wallet et Config/Transport/ExecutionPolicy/Store/Tauri restent hors Wallet.
- [ ] `0.2.6` — Introduire `ksp-app-wallet-desk` utilisant Config composite + Wallet + transport HTTP. `pre.001` fixe le sizing et le gabarit ; `pre.002` matérialise la crate Tauri et son shell splash/main avec Config + Logging bootstrap, ports `1432/1433`, Bootstrap, Font Awesome, DataTables/Select, SimpleBar, resize-observer-polyfill, TS-RS et bridge frontend Logging. Les tranches suivantes ajoutent `cfg.std.wallet`, inventory, lifecycle VIEW/OWNER, secrets `KSP_SECRET_WALLET_PASS_*`, identité autorisée + `getBalance`, puis administration/import/export, avec une dernière tranche prévue pour README/USAGE/docs/validation, prompt `0.2.7` et build Tauri final. - [ ] `0.2.6` — Introduire `ksp-app-wallet-desk` utilisant Config composite + Wallet + transport HTTP. `pre.001` fixe le sizing et le gabarit ; `pre.002` matérialise la crate Tauri et son shell splash/main avec Config + Logging bootstrap, ports `1432/1433`, Bootstrap, Font Awesome, DataTables/Select, SimpleBar, resize-observer-polyfill, TS-RS et bridge frontend Logging. `pre.003` matérialise `cfg.std.wallet`/`schema.std.wallet`, le composite `cfg.composite.ksp-app-wallet-desk`, `ResolvedWalletConfig`, `wallets_directory` global + `wallets_subdirectory` par profil et la préparation automatique des répertoires côté application avec logs debug/error. Les tranches suivantes ajoutent inventory, lifecycle VIEW/OWNER, secrets `KSP_SECRET_WALLET_PASS_*`, identité autorisée + `getBalance`, puis administration/import/export, avec une dernière tranche prévue pour README/USAGE/docs/validation, prompt `0.2.7` et build Tauri final.
- [ ] `0.2.7` — Étendre `ksp-onchain-transport-lib` au WebSocket Solana standard complet ; permettre plusieurs sessions sur une même URL sans imposer encore un pool automatique complexe. - [ ] `0.2.7` — Étendre `ksp-onchain-transport-lib` au WebSocket Solana standard complet ; permettre plusieurs sessions sur une même URL sans imposer encore un pool automatique complexe.
- [ ] `0.2.8` — Ajouter Helius LaserStream WebSocket comme extension du moteur WebSocket standard, sans duplication de client. - [ ] `0.2.8` — Ajouter Helius LaserStream WebSocket comme extension du moteur WebSocket standard, sans duplication de client.
- [ ] `0.2.9` — Ajouter une première fondation Yellowstone gRPC standard/provider-neutral ; dimensionner la surface exacte à `pre.001` selon la documentation normative actuelle. - [ ] `0.2.9` — Ajouter une première fondation Yellowstone gRPC standard/provider-neutral ; dimensionner la surface exacte à `pre.001` selon la documentation normative actuelle.

View File

@@ -0,0 +1,66 @@
{
"format_version": 1,
"default_profile": "devnet",
"profiles": [
{
"profile_id": "devnet",
"documents": [
{
"component_id": "logging",
"file_id": "cfg.std.logging",
"profile_id": "local_dev"
},
{
"component_id": "transport",
"file_id": "cfg.std.transport",
"profile_id": "devnet_public"
},
{
"component_id": "wallet",
"file_id": "cfg.std.wallet",
"profile_id": "default"
}
]
},
{
"profile_id": "temporary",
"documents": [
{
"component_id": "logging",
"file_id": "cfg.std.logging",
"profile_id": "local_dev"
},
{
"component_id": "transport",
"file_id": "cfg.std.transport",
"profile_id": "devnet_public"
},
{
"component_id": "wallet",
"file_id": "cfg.std.wallet",
"profile_id": "temporary"
}
]
},
{
"profile_id": "tests",
"documents": [
{
"component_id": "logging",
"file_id": "cfg.std.logging",
"profile_id": "local_dev"
},
{
"component_id": "transport",
"file_id": "cfg.std.transport",
"profile_id": "devnet_public"
},
{
"component_id": "wallet",
"file_id": "cfg.std.wallet",
"profile_id": "tests"
}
]
}
]
}

View File

@@ -0,0 +1,66 @@
{
"format_version": 1,
"default_profile": "devnet",
"profiles": [
{
"profile_id": "devnet",
"documents": [
{
"component_id": "logging",
"file_id": "cfg.std.logging",
"profile_id": "local_dev"
},
{
"component_id": "transport",
"file_id": "cfg.std.transport",
"profile_id": "devnet_public"
},
{
"component_id": "wallet",
"file_id": "cfg.std.wallet",
"profile_id": "default"
}
]
},
{
"profile_id": "temporary",
"documents": [
{
"component_id": "logging",
"file_id": "cfg.std.logging",
"profile_id": "local_dev"
},
{
"component_id": "transport",
"file_id": "cfg.std.transport",
"profile_id": "devnet_public"
},
{
"component_id": "wallet",
"file_id": "cfg.std.wallet",
"profile_id": "temporary"
}
]
},
{
"profile_id": "tests",
"documents": [
{
"component_id": "logging",
"file_id": "cfg.std.logging",
"profile_id": "local_dev"
},
{
"component_id": "transport",
"file_id": "cfg.std.transport",
"profile_id": "devnet_public"
},
{
"component_id": "wallet",
"file_id": "cfg.std.wallet",
"profile_id": "tests"
}
]
}
]
}

View File

@@ -0,0 +1,18 @@
{
"format_version": 1,
"wallets_directory": "${KSP_WALLETS_DIRECTORY:-wallets}",
"default_profile": "tests",
"profiles": [
{
"profile_id": "default"
},
{
"profile_id": "scenarios",
"wallets_subdirectory": "scenarios/ephemeral"
},
{
"profile_id": "tests",
"wallets_subdirectory": "tests"
}
]
}

View File

@@ -0,0 +1,54 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "urn:ksp:schema:std.wallet:v1",
"title": "KSP standard Wallet configuration",
"type": "object",
"additionalProperties": false,
"required": [
"format_version",
"wallets_directory",
"default_profile",
"profiles"
],
"properties": {
"format_version": {
"const": 1
},
"wallets_directory": {
"type": "string",
"minLength": 1
},
"default_profile": {
"$ref": "#/$defs/profileId"
},
"profiles": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/profile"
}
}
},
"$defs": {
"profileId": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9._-]*$"
},
"profile": {
"type": "object",
"additionalProperties": false,
"required": [
"profile_id"
],
"properties": {
"profile_id": {
"$ref": "#/$defs/profileId"
},
"wallets_subdirectory": {
"type": "string",
"minLength": 1
}
}
}
}
}

18
config/std.wallet.json Normal file
View File

@@ -0,0 +1,18 @@
{
"format_version": 1,
"wallets_directory": "${KSP_WALLETS_DIRECTORY:-wallets}",
"default_profile": "default",
"profiles": [
{
"profile_id": "default"
},
{
"profile_id": "temporary",
"wallets_subdirectory": "temporary"
},
{
"profile_id": "tests",
"wallets_subdirectory": "tests"
}
]
}

View File

@@ -37,9 +37,9 @@
<div class="d-flex align-items-center justify-content-between mb-4"> <div class="d-flex align-items-center justify-content-between mb-4">
<div> <div>
<h1 id="viewTitle" class="h3 mb-1">Dashboard</h1> <h1 id="viewTitle" class="h3 mb-1">Dashboard</h1>
<p class="text-body-secondary mb-0">Fondation UI de Wallet Desk — fonctionnalités Wallet branchées dans les tranches suivantes.</p> <p class="text-body-secondary mb-0">Composition Config Wallet Desk active — inventory Wallet branché en pre.004.</p>
</div> </div>
<span class="badge text-bg-secondary">0.2.6-pre.002</span> <span class="badge text-bg-secondary">0.2.6-pre.003</span>
</div> </div>
<section data-view-panel="dashboard"> <section data-view-panel="dashboard">
<div class="row g-3"> <div class="row g-3">
@@ -51,8 +51,14 @@
<dt class="col-sm-5">Version</dt><dd id="runtimeVersion" class="col-sm-7"></dd> <dt class="col-sm-5">Version</dt><dd id="runtimeVersion" class="col-sm-7"></dd>
<dt class="col-sm-5">Phase</dt><dd id="runtimeShellPhase" class="col-sm-7"></dd> <dt class="col-sm-5">Phase</dt><dd id="runtimeShellPhase" class="col-sm-7"></dd>
<dt class="col-sm-5">Documents Config enregistrés</dt><dd id="runtimeConfigDocuments" class="col-sm-7"></dd> <dt class="col-sm-5">Documents Config enregistrés</dt><dd id="runtimeConfigDocuments" class="col-sm-7"></dd>
<dt class="col-sm-5">Profil composite</dt><dd id="runtimeCompositeProfile" class="col-sm-7"></dd>
<dt class="col-sm-5">Profil Logging</dt><dd id="runtimeLoggingProfile" class="col-sm-7"></dd> <dt class="col-sm-5">Profil Logging</dt><dd id="runtimeLoggingProfile" class="col-sm-7"></dd>
<dt class="col-sm-5">Profil Wallet</dt><dd id="runtimeWalletProfile" class="col-sm-7"></dd>
<dt class="col-sm-5">Fallback Logging</dt><dd id="runtimeLoggingFallback" class="col-sm-7"></dd> <dt class="col-sm-5">Fallback Logging</dt><dd id="runtimeLoggingFallback" class="col-sm-7"></dd>
<dt class="col-sm-5">Racine Wallet</dt><dd id="runtimeWalletsDirectory" class="col-sm-7 text-break"></dd>
<dt class="col-sm-5">Sous-répertoire Wallet</dt><dd id="runtimeWalletsSubdirectory" class="col-sm-7 text-break"></dd>
<dt class="col-sm-5">Répertoire effectif</dt><dd id="runtimeEffectiveWalletsDirectory" class="col-sm-7 text-break"></dd>
<dt class="col-sm-5">Créé au démarrage</dt><dd id="runtimeWalletDirectoryCreated" class="col-sm-7"></dd>
</dl> </dl>
</div> </div>
</div> </div>
@@ -96,7 +102,7 @@
</div> </div>
</main> </main>
<footer class="app-footer bg-dark text-white-50 d-flex align-items-center px-4"> <footer class="app-footer bg-dark text-white-50 d-flex align-items-center px-4">
<span>KSP Wallet Desk · shell pre.002</span> <span>KSP Wallet Desk · Config composition pre.003</span>
</footer> </footer>
<script type="module" src="./ts/main.ts"></script> <script type="module" src="./ts/main.ts"></script>
</body> </body>

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/frontend/ts/main.ts // file: crates/ksp-app-wallet-desk/frontend/ts/main.ts
// version: 1 // version: 2
import "bootstrap"; import "bootstrap";
import DataTable from "datatables.net-bs5"; import DataTable from "datatables.net-bs5";
@@ -81,21 +81,33 @@ function initializeWalletTable(): void {
zeroRecords: "Aucun wallet correspondant.", zeroRecords: "Aucun wallet correspondant.",
}, },
}); });
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.002-shell" }); frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.003-config-wallet" });
} }
function renderRuntimeStatus(status: RuntimeStatusDto): void { function renderRuntimeStatus(status: RuntimeStatusDto): void {
const version = document.querySelector<HTMLElement>("#runtimeVersion"); const version = document.querySelector<HTMLElement>("#runtimeVersion");
const profile = document.querySelector<HTMLElement>("#runtimeLoggingProfile"); const compositeProfile = document.querySelector<HTMLElement>("#runtimeCompositeProfile");
const loggingProfile = document.querySelector<HTMLElement>("#runtimeLoggingProfile");
const walletProfile = document.querySelector<HTMLElement>("#runtimeWalletProfile");
const fallback = document.querySelector<HTMLElement>("#runtimeLoggingFallback"); const fallback = document.querySelector<HTMLElement>("#runtimeLoggingFallback");
const documents = document.querySelector<HTMLElement>("#runtimeConfigDocuments"); const documents = document.querySelector<HTMLElement>("#runtimeConfigDocuments");
const walletsRoot = document.querySelector<HTMLElement>("#runtimeWalletsDirectory");
const walletsSubdirectory = document.querySelector<HTMLElement>("#runtimeWalletsSubdirectory");
const effectiveWalletsDirectory = document.querySelector<HTMLElement>("#runtimeEffectiveWalletsDirectory");
const walletDirectoryCreated = document.querySelector<HTMLElement>("#runtimeWalletDirectoryCreated");
const phase = document.querySelector<HTMLElement>("#runtimeShellPhase"); const phase = document.querySelector<HTMLElement>("#runtimeShellPhase");
const shellStatus = document.querySelector<HTMLElement>("#shellStatus"); const shellStatus = document.querySelector<HTMLElement>("#shellStatus");
if (version) { if (version) {
version.textContent = status.applicationVersion; version.textContent = status.applicationVersion;
} }
if (profile) { if (compositeProfile) {
profile.textContent = status.activeLoggingProfile ?? "fallback transitoire"; compositeProfile.textContent = status.activeCompositeProfile;
}
if (loggingProfile) {
loggingProfile.textContent = status.activeLoggingProfile ?? "fallback transitoire";
}
if (walletProfile) {
walletProfile.textContent = status.activeWalletProfile;
} }
if (fallback) { if (fallback) {
fallback.textContent = status.fallbackLoggingActive ? "oui" : "non"; fallback.textContent = status.fallbackLoggingActive ? "oui" : "non";
@@ -103,15 +115,30 @@ function renderRuntimeStatus(status: RuntimeStatusDto): void {
if (documents) { if (documents) {
documents.textContent = status.configDocumentCount.toString(); documents.textContent = status.configDocumentCount.toString();
} }
if (walletsRoot) {
walletsRoot.textContent = status.walletsDirectory;
}
if (walletsSubdirectory) {
walletsSubdirectory.textContent = status.walletsSubdirectory ?? "— (racine globale)";
}
if (effectiveWalletsDirectory) {
effectiveWalletsDirectory.textContent = status.effectiveWalletsDirectory;
}
if (walletDirectoryCreated) {
walletDirectoryCreated.textContent = status.effectiveWalletsDirectoryCreatedOnStartup ? "oui" : "non, déjà présent";
}
if (phase) { if (phase) {
phase.textContent = status.shellPhase; phase.textContent = status.shellPhase;
} }
if (shellStatus) { if (shellStatus) {
shellStatus.textContent = "Shell Wallet Desk prêt."; shellStatus.textContent = "Config Wallet Desk résolue ; répertoire Wallet prêt.";
} }
frontendTrace("main", "Wallet Desk runtime status rendered", { frontendTrace("main", "Wallet Desk runtime status rendered", {
compositeProfile: status.activeCompositeProfile,
fallbackLoggingActive: status.fallbackLoggingActive, fallbackLoggingActive: status.fallbackLoggingActive,
shellPhase: status.shellPhase, shellPhase: status.shellPhase,
walletDirectoryCreatedOnStartup: status.effectiveWalletsDirectoryCreatedOnStartup,
walletProfile: status.activeWalletProfile,
}); });
} }

View File

@@ -1,7 +1,7 @@
{ {
"name": "ksp-app-wallet-desk", "name": "ksp-app-wallet-desk",
"private": true, "private": true,
"version": "0.2.6-pre.2", "version": "0.2.6-pre.3",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/app_state.rs // file: crates/ksp-app-wallet-desk/src/app_state.rs
// version: 2 // version: 3
//! Shared backend state owned by the Wallet Desk Tauri application. //! Shared backend state owned by the Wallet Desk Tauri application.
@@ -9,10 +9,11 @@ pub(crate) struct AppState {
logging_runtime: std::sync::Mutex<LoggingRuntimeState>, logging_runtime: std::sync::Mutex<LoggingRuntimeState>,
splash_settings: crate::SplashSettings, splash_settings: crate::SplashSettings,
splash_sequence_started: std::sync::atomic::AtomicBool, splash_sequence_started: std::sync::atomic::AtomicBool,
wallet_config_startup: crate::WalletConfigStartup,
} }
impl AppState { impl AppState {
/// Initializes Config ownership, Logging and the common desktop splash state. /// Initializes Config composition, Logging, Wallet directory preparation and the common desktop splash state.
pub(crate) fn initialize(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<Self> { pub(crate) fn initialize(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<Self> {
let config_management = crate::config_management(arguments); let config_management = crate::config_management(arguments);
let config_management = match config_management { let config_management = match config_management {
@@ -29,6 +30,14 @@ impl AppState {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
}; };
let wallet_config_startup = crate::initialize_wallet_config(&config_management);
let wallet_config_startup = match wallet_config_startup {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
ksp_logging_lib::error!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_BOOTSTRAP, error_domain = error.code().domain(), error_code = error.code().code(), "Wallet Desk Config/Wallet directory bootstrap failed");
return std::result::Result::Err(error);
},
};
let splash_settings = crate::SplashSettings::load(); let splash_settings = crate::SplashSettings::load();
let splash_settings = match splash_settings { let splash_settings = match splash_settings {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
@@ -48,10 +57,11 @@ impl AppState {
}), }),
splash_settings, splash_settings,
splash_sequence_started: std::sync::atomic::AtomicBool::new(false), splash_sequence_started: std::sync::atomic::AtomicBool::new(false),
wallet_config_startup,
}); });
} }
/// Builds the safe shell status DTO exposed during pre.002. /// Builds the safe Config-composition status DTO exposed during pre.003.
pub(crate) fn runtime_status(&self) -> ksp_core_lib::Result<crate::RuntimeStatusDto> { pub(crate) fn runtime_status(&self) -> ksp_core_lib::Result<crate::RuntimeStatusDto> {
let document_count = self.config_management.engine().registry().descriptors().count(); let document_count = self.config_management.engine().registry().descriptors().count();
let document_count = u32::try_from(document_count); let document_count = u32::try_from(document_count);
@@ -61,7 +71,7 @@ impl AppState {
return std::result::Result::Err( return std::result::Result::Err(
ksp_core_lib::Error::new( ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_INVALID, crate::ERROR_CODE_APP_STATE_INVALID,
"Config registry contains too many descriptors for the Wallet Desk shell DTO", "Config registry contains too many descriptors for the Wallet Desk runtime DTO",
) )
.with_source(error), .with_source(error),
); );
@@ -78,13 +88,22 @@ impl AppState {
}, },
}; };
let _keep_guard_alive = &runtime.guard; let _keep_guard_alive = &runtime.guard;
let resolved = self.wallet_config_startup.resolved();
let wallets_subdirectory = resolved.wallets_subdirectory().map(|value| return value.to_string_lossy().into_owned());
return std::result::Result::Ok(crate::RuntimeStatusDto { return std::result::Result::Ok(crate::RuntimeStatusDto {
application_version: env!("CARGO_PKG_VERSION").to_owned(), application_version: env!("CARGO_PKG_VERSION").to_owned(),
config_document_count: document_count, active_composite_profile: self.wallet_config_startup.composite_profile_id().to_owned(),
active_logging_profile: runtime.active_profile_id.clone(), active_logging_profile: runtime.active_profile_id.clone(),
active_wallet_profile: resolved.profile_id().to_owned(),
config_document_count: document_count,
effective_wallets_directory: resolved.effective_wallets_directory().to_string_lossy().into_owned(),
effective_wallets_directory_created_on_startup: self.wallet_config_startup.effective_directory_created_on_startup(),
fallback_logging_active: runtime.fallback_active, fallback_logging_active: runtime.fallback_active,
root_wallets_directory_created_on_startup: self.wallet_config_startup.root_directory_created_on_startup(),
shell_phase: "pre.003-config-wallet".to_owned(),
startup_diagnostic: runtime.startup_diagnostic.clone(), startup_diagnostic: runtime.startup_diagnostic.clone(),
shell_phase: "pre.002-shell".to_owned(), wallets_directory: resolved.wallets_directory().to_string_lossy().into_owned(),
wallets_subdirectory,
}); });
} }

View File

@@ -1,7 +1,7 @@
// file: crates/ksp-app-wallet-desk/src/bootstrap.rs // file: crates/ksp-app-wallet-desk/src/bootstrap.rs
// version: 1 // version: 2
//! Config and Logging bootstrap for Wallet Desk. //! Config composite and Logging bootstrap for Wallet Desk.
/// Crate-internal Logging startup state shared by the application state. /// Crate-internal Logging startup state shared by the application state.
pub(crate) struct LoggingStartup { pub(crate) struct LoggingStartup {
@@ -43,7 +43,7 @@ pub(crate) fn config_management(arguments: &[std::ffi::OsString]) -> ksp_core_li
return std::result::Result::Ok(ksp_config_lib::ConfigManagement::new(engine)); return std::result::Result::Ok(ksp_config_lib::ConfigManagement::new(engine));
} }
/// Initializes the Logging runtime from Config, with the same bounded fallback policy as Config Desk. /// Initializes the Logging runtime from the Wallet Desk composite, with the same bounded fallback policy as Config Desk.
pub(crate) fn initialize_logging( pub(crate) fn initialize_logging(
management: &ksp_config_lib::ConfigManagement, management: &ksp_config_lib::ConfigManagement,
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity, runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
@@ -54,7 +54,7 @@ pub(crate) fn initialize_logging(
let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity); let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity);
match guard { match guard {
std::result::Result::Ok(guard) => { std::result::Result::Ok(guard) => {
ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_BOOTSTRAP, active_profile = active_profile_id.as_str(), "initialized Wallet Desk logging from managed configuration"); ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_BOOTSTRAP, active_profile = active_profile_id.as_str(), "initialized Wallet Desk logging from composite-managed configuration");
std::result::Result::Ok(LoggingStartup { std::result::Result::Ok(LoggingStartup {
guard, guard,
active_profile_id: std::option::Option::Some(active_profile_id), active_profile_id: std::option::Option::Some(active_profile_id),
@@ -71,6 +71,47 @@ pub(crate) fn initialize_logging(
}; };
} }
/// Loads the concrete Wallet Desk Config composite using its registered logical `file_id` and autonomous default profile.
pub(crate) fn load_wallet_desk_composite(management: &ksp_config_lib::ConfigManagement) -> ksp_core_lib::Result<ksp_config_lib::ResolvedConfigComposite> {
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK);
let file_id = match file_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return management.engine().load_resolved_composite(&file_id, std::option::Option::None);
}
/// Returns one required standard profile from the Wallet Desk composite after validating the referenced logical `file_id`.
pub(crate) fn required_composite_component_profile(
composite: &ksp_config_lib::ResolvedConfigComposite,
component_id: &str,
expected_file_id: &str,
) -> ksp_core_lib::Result<ksp_config_lib::ResolvedConfigProfile> {
let component = composite.component(component_id);
let component = match component {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_CONFIG_COMPOSITE_INVALID, "Wallet Desk composite is missing a required component")
.with_context("composite_file_id", composite.file_id().as_str())
.with_context("composite_profile_id", composite.profile_id())
.with_context("component_id", component_id),
);
},
};
if component.resolved().file_id().as_str() != expected_file_id {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_CONFIG_COMPOSITE_INVALID, "Wallet Desk composite component references an unexpected Config document")
.with_context("composite_file_id", composite.file_id().as_str())
.with_context("composite_profile_id", composite.profile_id())
.with_context("component_id", component_id)
.with_context("expected_file_id", expected_file_id)
.with_context("actual_file_id", component.resolved().file_id().as_str()),
);
}
return std::result::Result::Ok(component.resolved().clone());
}
fn fallback_logging_settings() -> ksp_logging_lib::LoggingSettings { fn fallback_logging_settings() -> ksp_logging_lib::LoggingSettings {
return ksp_logging_lib::LoggingSettings::new( return ksp_logging_lib::LoggingSettings::new(
ksp_logging_lib::LogFilterLevel::Info, ksp_logging_lib::LogFilterLevel::Info,
@@ -86,7 +127,26 @@ fn resolve_logging_startup(management: &ksp_config_lib::ConfigManagement) -> Log
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return fallback_startup_plan(error), std::result::Result::Err(error) => return fallback_startup_plan(error),
}; };
let resolved = management.engine().load_resolved_logging_config(std::option::Option::None, &environment); let composite = crate::load_wallet_desk_composite(management);
let composite = match composite {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return fallback_startup_plan(error),
};
let logging_profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_LOGGING, ksp_config_lib::FILE_ID_STD_LOGGING);
let logging_profile = match logging_profile {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return fallback_startup_plan(error),
};
let transport_profile =
crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_TRANSPORT, ksp_config_lib::FILE_ID_STD_TRANSPORT);
if let std::result::Result::Err(error) = transport_profile {
return fallback_startup_plan(error);
}
let wallet_profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_WALLET, ksp_config_lib::FILE_ID_STD_WALLET);
if let std::result::Result::Err(error) = wallet_profile {
return fallback_startup_plan(error);
}
let resolved = management.engine().resolve_logging_config_profile(&logging_profile, &environment);
let resolved = match resolved { let resolved = match resolved {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return fallback_startup_plan(error), std::result::Result::Err(error) => return fallback_startup_plan(error),

View File

@@ -1,14 +1,22 @@
// file: crates/ksp-app-wallet-desk/src/constants.rs // file: crates/ksp-app-wallet-desk/src/constants.rs
// version: 1 // version: 2
//! Logging targets and domains owned by Wallet Desk. //! Logging targets, domains and composite component identifiers owned by Wallet Desk.
/// Composite-local identifier for the Logging standard document.
pub(crate) const COMPOSITE_COMPONENT_ID_LOGGING: &str = "logging";
/// Composite-local identifier for the HTTP Transport standard document.
pub(crate) const COMPOSITE_COMPONENT_ID_TRANSPORT: &str = "transport";
/// Composite-local identifier for the Wallet standard document.
pub(crate) const COMPOSITE_COMPONENT_ID_WALLET: &str = "wallet";
/// Structured domain used while bootstrapping Config and Logging. /// Structured domain used while bootstrapping Config and Logging.
pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "wallet.bootstrap"; pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "wallet.bootstrap";
/// Structured domain used by technical frontend events. /// Structured domain used by technical frontend events.
pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend"; pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
/// Structured domain used by the Wallet Desk shell. /// Structured domain used by the Wallet Desk shell.
pub(crate) const TRACING_DOMAIN_SHELL: &str = "wallet.shell"; pub(crate) const TRACING_DOMAIN_SHELL: &str = "wallet.shell";
/// Structured domain used while preparing Wallet filesystem roots.
pub(crate) const TRACING_DOMAIN_WALLET_CONFIG: &str = "wallet.config";
/// Structured domain used by Tauri window lifecycle operations. /// Structured domain used by Tauri window lifecycle operations.
pub(crate) const TRACING_DOMAIN_WINDOWS: &str = "desktop.window"; pub(crate) const TRACING_DOMAIN_WINDOWS: &str = "desktop.window";
/// Owning target for backend events emitted by Wallet Desk. /// Owning target for backend events emitted by Wallet Desk.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/dto_common.rs // file: crates/ksp-app-wallet-desk/src/dto_common.rs
// version: 1 // version: 2
//! Common Tauri DTOs shared by Wallet Desk shell commands. //! Common Tauri DTOs shared by Wallet Desk shell commands.
@@ -30,23 +30,37 @@ impl CommandErrorDto {
} }
} }
/// Initial application/runtime snapshot exposed to the Wallet Desk shell. /// Safe application/runtime snapshot exposed to the Wallet Desk shell.
#[derive(Clone, Debug, serde::Serialize, TS)] #[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/dto_common/RuntimeStatusDto.ts")] #[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/dto_common/RuntimeStatusDto.ts")]
pub(crate) struct RuntimeStatusDto { pub(crate) struct RuntimeStatusDto {
/// Cargo application version. /// Cargo application version.
pub(crate) application_version: String, pub(crate) application_version: String,
/// Number of documents registered by Config before Wallet-specific documents are introduced. /// Wallet Desk composite profile selected during bootstrap.
pub(crate) config_document_count: u32, pub(crate) active_composite_profile: String,
/// Active configured Logging profile, or `None` while the transient fallback runtime is active. /// Active configured Logging profile, or `None` while the transient fallback runtime is active.
pub(crate) active_logging_profile: std::option::Option<String>, pub(crate) active_logging_profile: std::option::Option<String>,
/// Standard Wallet profile selected by the active composite.
pub(crate) active_wallet_profile: String,
/// Number of logical Config resources registered by the current application runtime.
pub(crate) config_document_count: u32,
/// Effective Wallet directory after applying the optional profile subdirectory.
pub(crate) effective_wallets_directory: String,
/// Whether bootstrap created any missing component of the effective Wallet profile directory.
pub(crate) effective_wallets_directory_created_on_startup: bool,
/// Whether Wallet Desk had to install its transient in-memory Logging fallback. /// Whether Wallet Desk had to install its transient in-memory Logging fallback.
pub(crate) fallback_logging_active: bool, pub(crate) fallback_logging_active: bool,
/// Whether bootstrap created the configured global Wallet root.
pub(crate) root_wallets_directory_created_on_startup: bool,
/// Current implementation phase exposed for the Config composition tranche.
pub(crate) shell_phase: String,
/// Safe startup diagnostic that caused fallback Logging, when applicable. /// Safe startup diagnostic that caused fallback Logging, when applicable.
pub(crate) startup_diagnostic: std::option::Option<CommandErrorDto>, pub(crate) startup_diagnostic: std::option::Option<CommandErrorDto>,
/// Current implementation phase exposed for the pre.002 shell. /// Resolved global Wallet root configured by `cfg.std.wallet`.
pub(crate) shell_phase: String, pub(crate) wallets_directory: String,
/// Optional profile-relative Wallet subdirectory selected by the active composite.
pub(crate) wallets_subdirectory: std::option::Option<String>,
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -1,12 +1,14 @@
// file: crates/ksp-app-wallet-desk/src/errors.rs // file: crates/ksp-app-wallet-desk/src/errors.rs
// version: 1 // version: 2
//! Application-local error codes for the Wallet Desk shell. //! Application-local error codes for Wallet Desk composition and desktop runtime surfaces.
/// Shared Wallet Desk application state is internally inconsistent. /// Shared Wallet Desk application state is internally inconsistent.
pub(crate) const ERROR_CODE_APP_STATE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "app_state_invalid"); pub(crate) const ERROR_CODE_APP_STATE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "app_state_invalid");
/// Shared Wallet Desk runtime state cannot be locked safely. /// Shared Wallet Desk runtime state cannot be locked safely.
pub(crate) const ERROR_CODE_APP_STATE_LOCK_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "app_state_lock_failed"); pub(crate) const ERROR_CODE_APP_STATE_LOCK_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "app_state_lock_failed");
/// The Wallet Desk Config composite does not expose one of its required standard components with the expected `file_id`.
pub(crate) const ERROR_CODE_CONFIG_COMPOSITE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "config_composite_invalid");
/// Frontend logging requested an unsupported level. /// Frontend logging requested an unsupported level.
pub(crate) const ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "frontend_log_level_invalid"); pub(crate) const ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "frontend_log_level_invalid");
/// Frontend logging requested a target outside the application whitelist. /// Frontend logging requested a target outside the application whitelist.
@@ -24,3 +26,8 @@ pub(crate) const ERROR_CODE_TAURI_WINDOW_MISSING: ksp_core_lib::ErrorCode = ksp_
/// A Tauri window show/focus/destroy/event operation failed. /// A Tauri window show/focus/destroy/event operation failed.
pub(crate) const ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED: ksp_core_lib::ErrorCode = pub(crate) const ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("wallet_desk", "tauri_window_operation_failed"); ksp_core_lib::ErrorCode::new("wallet_desk", "tauri_window_operation_failed");
/// The configured Wallet root or effective profile directory resolves to an unsupported filesystem object.
pub(crate) const ERROR_CODE_WALLET_DIRECTORY_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_directory_invalid");
/// Wallet Desk could not inspect or create the configured Wallet directory tree.
pub(crate) const ERROR_CODE_WALLET_DIRECTORY_PREPARE_FAILED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_directory_prepare_failed");

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/lib.rs // file: crates/ksp-app-wallet-desk/src/lib.rs
// version: 1 // version: 2
//! Tauri desktop application shell for KSP Wallet management and inspection. //! Tauri desktop application shell for KSP Wallet management and inspection.
@@ -18,6 +18,7 @@ mod splash;
mod tauri; mod tauri;
mod tw_main; mod tw_main;
mod tw_splash; mod tw_splash;
mod wallet_config;
/// Runs the KSP wallet desktop application. /// Runs the KSP wallet desktop application.
pub use self::tauri::run; pub use self::tauri::run;
@@ -30,12 +31,24 @@ pub(crate) use self::bootstrap::LoggingStartup;
pub(crate) use self::bootstrap::config_management; pub(crate) use self::bootstrap::config_management;
/// Initializes the Logging runtime from Config, with the same bounded fallback policy as Config Desk. /// Initializes the Logging runtime from Config, with the same bounded fallback policy as Config Desk.
pub(crate) use self::bootstrap::initialize_logging; pub(crate) use self::bootstrap::initialize_logging;
/// Loads the concrete Wallet Desk Config composite.
pub(crate) use self::bootstrap::load_wallet_desk_composite;
/// Resolves and validates one required standard-document component from the Wallet Desk composite.
pub(crate) use self::bootstrap::required_composite_component_profile;
/// Composite-local identifier for the Logging standard document.
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_LOGGING;
/// Composite-local identifier for the HTTP Transport standard document.
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_TRANSPORT;
/// Composite-local identifier for the Wallet standard document.
pub(crate) use self::constants::COMPOSITE_COMPONENT_ID_WALLET;
/// Structured domain used while bootstrapping Config and Logging. /// Structured domain used while bootstrapping Config and Logging.
pub(crate) use self::constants::TRACING_DOMAIN_BOOTSTRAP; pub(crate) use self::constants::TRACING_DOMAIN_BOOTSTRAP;
/// Structured domain used by technical frontend events. /// Structured domain used by technical frontend events.
pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND; pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND;
/// Structured domain used by the Wallet Desk shell. /// Structured domain used by the Wallet Desk shell.
pub(crate) use self::constants::TRACING_DOMAIN_SHELL; pub(crate) use self::constants::TRACING_DOMAIN_SHELL;
/// Structured domain used while preparing Wallet filesystem roots.
pub(crate) use self::constants::TRACING_DOMAIN_WALLET_CONFIG;
/// Structured domain used by Tauri window lifecycle operations. /// Structured domain used by Tauri window lifecycle operations.
pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS; pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS;
/// Owning target for backend events emitted by Wallet Desk. /// Owning target for backend events emitted by Wallet Desk.
@@ -54,6 +67,8 @@ pub(crate) use self::dto_common::RuntimeStatusDto;
pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID; pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID;
/// Shared Wallet Desk runtime state cannot be locked safely. /// Shared Wallet Desk runtime state cannot be locked safely.
pub(crate) use self::errors::ERROR_CODE_APP_STATE_LOCK_FAILED; pub(crate) use self::errors::ERROR_CODE_APP_STATE_LOCK_FAILED;
/// The Wallet Desk Config composite is missing or misroutes one required standard component.
pub(crate) use self::errors::ERROR_CODE_CONFIG_COMPOSITE_INVALID;
/// Frontend logging requested an unsupported level. /// Frontend logging requested an unsupported level.
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID; pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID;
/// Frontend logging requested a target outside the application whitelist. /// Frontend logging requested a target outside the application whitelist.
@@ -70,6 +85,10 @@ pub(crate) use self::errors::ERROR_CODE_TAURI_RUNTIME_FAILED;
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_MISSING; pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_MISSING;
/// A Tauri window show/focus/destroy/event operation failed. /// A Tauri window show/focus/destroy/event operation failed.
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED; pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED;
/// The configured Wallet root or effective profile directory resolves to an unsupported filesystem object.
pub(crate) use self::errors::ERROR_CODE_WALLET_DIRECTORY_INVALID;
/// Wallet Desk could not inspect or create the configured Wallet directory tree.
pub(crate) use self::errors::ERROR_CODE_WALLET_DIRECTORY_PREPARE_FAILED;
/// Log payload sent by Wallet Desk frontend scripts. /// Log payload sent by Wallet Desk frontend scripts.
pub(crate) use self::frontend_logging::FrontendLogPayloadDto; pub(crate) use self::frontend_logging::FrontendLogPayloadDto;
/// Emits one validated frontend event through the KSP Logging facade. /// Emits one validated frontend event through the KSP Logging facade.
@@ -88,3 +107,7 @@ pub(crate) use self::tw_main::show_main_window;
pub(crate) use self::tw_splash::require_splash_window; pub(crate) use self::tw_splash::require_splash_window;
/// Starts the one-shot splash lifecycle after the splash frontend reports readiness. /// Starts the one-shot splash lifecycle after the splash frontend reports readiness.
pub(crate) use self::tw_splash::splash_frontend_ready_service; pub(crate) use self::tw_splash::splash_frontend_ready_service;
/// Resolved Wallet Config and directory preparation status captured during application bootstrap.
pub(crate) use self::wallet_config::WalletConfigStartup;
/// Resolves the composite-selected Wallet Config and prepares its application-owned directory tree.
pub(crate) use self::wallet_config::initialize_wallet_config;

View File

@@ -0,0 +1,178 @@
// file: crates/ksp-app-wallet-desk/src/wallet_config.rs
// version: 1
//! Wallet Desk composition adapter for the standard Wallet Config and its application-owned directory preparation.
/// Resolved Wallet Config and directory preparation status captured during application bootstrap.
pub(crate) struct WalletConfigStartup {
composite_profile_id: String,
effective_directory_created_on_startup: bool,
resolved: ksp_config_lib::ResolvedWalletConfig,
root_directory_created_on_startup: bool,
}
impl WalletConfigStartup {
/// Returns the selected Wallet Desk composite profile.
pub(crate) fn composite_profile_id(&self) -> &str {
return self.composite_profile_id.as_str();
}
/// Reports whether bootstrap created at least one missing component of the effective Wallet profile directory.
pub(crate) const fn effective_directory_created_on_startup(&self) -> bool {
return self.effective_directory_created_on_startup;
}
/// Returns the resolved standard Wallet configuration.
pub(crate) const fn resolved(&self) -> &ksp_config_lib::ResolvedWalletConfig {
return &self.resolved;
}
/// Reports whether bootstrap created the configured global Wallet root.
pub(crate) const fn root_directory_created_on_startup(&self) -> bool {
return self.root_directory_created_on_startup;
}
}
/// Resolves the Wallet component selected by the Wallet Desk composite and prepares its global/effective directory tree.
pub(crate) fn initialize_wallet_config(management: &ksp_config_lib::ConfigManagement) -> ksp_core_lib::Result<WalletConfigStartup> {
let environment = ksp_config_lib::ConfigEnvironment::load();
let environment = match environment {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let composite = crate::load_wallet_desk_composite(management);
let composite = match composite {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let logging_profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_LOGGING, ksp_config_lib::FILE_ID_STD_LOGGING);
if let std::result::Result::Err(error) = logging_profile {
return std::result::Result::Err(error);
}
let transport_profile =
crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_TRANSPORT, ksp_config_lib::FILE_ID_STD_TRANSPORT);
if let std::result::Result::Err(error) = transport_profile {
return std::result::Result::Err(error);
}
let wallet_profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_WALLET, ksp_config_lib::FILE_ID_STD_WALLET);
let wallet_profile = match wallet_profile {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let resolved = management.engine().resolve_wallet_config_profile(&wallet_profile, &environment);
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let preparation = prepare_wallet_directories(&resolved);
let preparation = match preparation {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let root_path = resolved.wallets_directory().to_string_lossy().into_owned();
let effective_path = resolved.effective_wallets_directory().to_string_lossy().into_owned();
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_CONFIG, composite_profile = composite.profile_id(), wallet_profile = resolved.profile_id(), root_path = root_path.as_str(), effective_path = effective_path.as_str(), root_created = preparation.root_created, effective_created = preparation.effective_created, "Wallet Desk Wallet directories are ready");
return std::result::Result::Ok(WalletConfigStartup {
composite_profile_id: composite.profile_id().to_owned(),
effective_directory_created_on_startup: preparation.effective_created,
resolved,
root_directory_created_on_startup: preparation.root_created,
});
}
struct WalletDirectoryPreparation {
effective_created: bool,
root_created: bool,
}
fn prepare_wallet_directories(resolved: &ksp_config_lib::ResolvedWalletConfig) -> ksp_core_lib::Result<WalletDirectoryPreparation> {
let root_created = ensure_global_wallet_root(resolved.wallets_directory());
let root_created = match root_created {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let effective_created = match resolved.wallets_subdirectory() {
std::option::Option::Some(subdirectory) => {
let created = ensure_profile_wallet_directory(resolved.wallets_directory(), subdirectory);
match created {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
},
std::option::Option::None => root_created,
};
return std::result::Result::Ok(WalletDirectoryPreparation { effective_created, root_created });
}
fn ensure_global_wallet_root(path: &std::path::Path) -> ksp_core_lib::Result<bool> {
let metadata = std::fs::metadata(path);
return match metadata {
std::result::Result::Ok(metadata) if metadata.is_dir() => std::result::Result::Ok(false),
std::result::Result::Ok(_) => directory_invalid(path, "configured Wallet root exists but is not a directory"),
std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
let creation = std::fs::create_dir_all(path);
match creation {
std::result::Result::Ok(()) => std::result::Result::Ok(true),
std::result::Result::Err(error) => directory_prepare_error(path, "configured Wallet root cannot be created", error),
}
},
std::result::Result::Err(error) => directory_prepare_error(path, "configured Wallet root cannot be inspected", error),
};
}
fn ensure_profile_wallet_directory(root: &std::path::Path, subdirectory: &std::path::Path) -> ksp_core_lib::Result<bool> {
let mut current = root.to_path_buf();
let mut created = false;
for component in subdirectory.components() {
let normal = match component {
std::path::Component::Normal(value) => value,
_ => return directory_invalid(subdirectory, "Wallet profile subdirectory contains an unsafe path component"),
};
current.push(normal);
let metadata = std::fs::symlink_metadata(current.as_path());
match metadata {
std::result::Result::Ok(metadata) if metadata.file_type().is_symlink() => {
return directory_invalid(current.as_path(), "Wallet profile directory path traverses a symbolic link");
},
std::result::Result::Ok(metadata) if metadata.is_dir() => {},
std::result::Result::Ok(_) => return directory_invalid(current.as_path(), "Wallet profile directory path contains a non-directory entry"),
std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
let creation = std::fs::create_dir(current.as_path());
if let std::result::Result::Err(error) = creation {
return directory_prepare_error(current.as_path(), "Wallet profile directory component cannot be created", error);
}
created = true;
},
std::result::Result::Err(error) => {
return directory_prepare_error(current.as_path(), "Wallet profile directory component cannot be inspected", error);
},
}
}
return std::result::Result::Ok(created);
}
fn directory_invalid<T>(path: &std::path::Path, reason: &'static str) -> ksp_core_lib::Result<T> {
let path_text = path.to_string_lossy().into_owned();
ksp_logging_lib::error!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_CONFIG, path = path_text.as_str(), reason, "Wallet directory preparation rejected an invalid filesystem object");
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_WALLET_DIRECTORY_INVALID, "Configured Wallet directory is invalid")
.with_context("path", path_text)
.with_context("reason", reason),
);
}
fn directory_prepare_error<T>(path: &std::path::Path, reason: &'static str, source: std::io::Error) -> ksp_core_lib::Result<T> {
let path_text = path.to_string_lossy().into_owned();
let source_kind = std::format!("{:?}", source.kind());
ksp_logging_lib::error!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_CONFIG, path = path_text.as_str(), reason, source_kind = source_kind.as_str(), "Wallet directory preparation failed");
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_WALLET_DIRECTORY_PREPARE_FAILED, "Configured Wallet directory cannot be prepared")
.with_context("path", path_text)
.with_context("reason", reason)
.with_source(source),
);
}
#[cfg(test)]
#[path = "../unit_tests/wallet_config.rs"]
mod tests;

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "KSP Wallet Desk", "productName": "KSP Wallet Desk",
"version": "0.2.6-pre.2", "version": "0.2.6-pre.3",
"identifier": "com.sasedev.ksp-app-wallet-desk", "identifier": "com.sasedev.ksp-app-wallet-desk",
"build": { "build": {
"beforeDevCommand": "npm run dev", "beforeDevCommand": "npm run dev",

View File

@@ -0,0 +1,48 @@
// file: crates/ksp-app-wallet-desk/tests/config_composition.rs
// version: 1
//! Config composition contracts introduced for Wallet Desk pre.003.
fn workspace_engine() -> ksp_core_lib::Result<ksp_config_lib::ConfigDocumentEngine> {
let workspace = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas"));
let bootstrap = match bootstrap {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let registry = ksp_config_lib::ConfigFileRegistry::defaults();
return match registry {
std::result::Result::Ok(value) => std::result::Result::Ok(ksp_config_lib::ConfigDocumentEngine::new(bootstrap, value)),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
#[test]
fn wallet_desk_composite_selects_logging_transport_and_wallet_by_file_id() {
let engine = workspace_engine();
assert!(engine.is_ok(), "workspace Config engine should be constructible: {engine:?}");
if let std::result::Result::Ok(engine) = engine {
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK);
assert!(file_id.is_ok(), "Wallet Desk composite file_id should be valid: {file_id:?}");
if let std::result::Result::Ok(file_id) = file_id {
let composite = engine.load_resolved_composite(&file_id, std::option::Option::None);
assert!(composite.is_ok(), "committed Wallet Desk composite should resolve: {composite:?}");
if let std::result::Result::Ok(composite) = composite {
assert_eq!(composite.profile_id(), "devnet");
assert_component(&composite, "logging", ksp_config_lib::FILE_ID_STD_LOGGING, "local_dev");
assert_component(&composite, "transport", ksp_config_lib::FILE_ID_STD_TRANSPORT, "devnet_public");
assert_component(&composite, "wallet", ksp_config_lib::FILE_ID_STD_WALLET, "default");
}
}
}
}
fn assert_component(composite: &ksp_config_lib::ResolvedConfigComposite, component_id: &str, file_id: &str, profile_id: &str) {
let component = composite.component(component_id);
assert!(component.is_some(), "missing component {component_id}");
if let std::option::Option::Some(component) = component {
assert_eq!(component.resolved().file_id().as_str(), file_id);
assert_eq!(component.resolved().profile_id(), profile_id);
assert_eq!(component.resolved().selection_source(), ksp_config_lib::ConfigProfileSelectionSource::Composite);
}
}

View File

@@ -1,7 +1,7 @@
// file: crates/ksp-app-wallet-desk/tests/desktop_contract.rs // file: crates/ksp-app-wallet-desk/tests/desktop_contract.rs
// version: 1 // version: 2
//! Desktop build and shell contract audits for Wallet Desk pre.002. //! Desktop build, shell and Config-status contract audits for Wallet Desk.
fn app_root() -> std::path::PathBuf { fn app_root() -> std::path::PathBuf {
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
@@ -78,6 +78,8 @@ fn shell_contains_wallet_navigation_datatable_and_lock_state_icons() {
assert!(html.contains("fa-lock")); assert!(html.contains("fa-lock"));
assert!(html.contains("fa-lock-open")); assert!(html.contains("fa-lock-open"));
assert!(html.contains("data-view=\"wallets\"")); assert!(html.contains("data-view=\"wallets\""));
assert!(html.contains("id=\"runtimeCompositeProfile\""));
assert!(html.contains("id=\"runtimeEffectiveWalletsDirectory\""));
assert!(main.contains("new DataTable")); assert!(main.contains("new DataTable"));
assert!(main.contains("datatables.net-select-bs5")); assert!(main.contains("datatables.net-select-bs5"));
assert!(main.contains("ResizeObserver")); assert!(main.contains("ResizeObserver"));

View File

@@ -0,0 +1,79 @@
// file: crates/ksp-app-wallet-desk/unit_tests/wallet_config.rs
// version: 1
#[test]
fn directory_preparation_creates_missing_root_and_nested_profile_path() {
let root = fixture_path("create");
cleanup_fixture(root.as_path());
let root_created = super::ensure_global_wallet_root(root.as_path());
assert_eq!(root_created.as_ref().ok().copied(), std::option::Option::Some(true));
let profile_created = super::ensure_profile_wallet_directory(root.as_path(), std::path::Path::new("tests/ephemeral"));
assert_eq!(profile_created.as_ref().ok().copied(), std::option::Option::Some(true));
assert!(root.join("tests/ephemeral").is_dir());
let root_again = super::ensure_global_wallet_root(root.as_path());
assert_eq!(root_again.as_ref().ok().copied(), std::option::Option::Some(false));
let profile_again = super::ensure_profile_wallet_directory(root.as_path(), std::path::Path::new("tests/ephemeral"));
assert_eq!(profile_again.as_ref().ok().copied(), std::option::Option::Some(false));
cleanup_fixture(root.as_path());
}
#[test]
fn directory_preparation_rejects_existing_non_directory_root() {
let root = fixture_path("file-root");
cleanup_fixture(root.as_path());
let parent = root.parent();
assert!(parent.is_some());
if let std::option::Option::Some(parent) = parent {
let created = std::fs::create_dir_all(parent);
assert!(created.is_ok(), "fixture parent should be creatable: {created:?}");
let written = std::fs::write(root.as_path(), b"not-a-directory");
assert!(written.is_ok(), "fixture file should be writable: {written:?}");
let result = super::ensure_global_wallet_root(root.as_path());
assert!(result.is_err(), "non-directory Wallet root should fail: {result:?}");
if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_WALLET_DIRECTORY_INVALID);
}
}
cleanup_fixture(root.as_path());
}
#[cfg(unix)]
#[test]
fn profile_directory_preparation_rejects_symbolic_link_components() {
let root = fixture_path("symlink-root");
let outside = fixture_path("symlink-outside");
cleanup_fixture(root.as_path());
cleanup_fixture(outside.as_path());
let root_created = std::fs::create_dir_all(root.as_path());
let outside_created = std::fs::create_dir_all(outside.as_path());
assert!(root_created.is_ok(), "root fixture should be creatable: {root_created:?}");
assert!(outside_created.is_ok(), "outside fixture should be creatable: {outside_created:?}");
if root_created.is_ok() && outside_created.is_ok() {
let linked = std::os::unix::fs::symlink(outside.as_path(), root.join("tests"));
assert!(linked.is_ok(), "symlink fixture should be creatable: {linked:?}");
let result = super::ensure_profile_wallet_directory(root.as_path(), std::path::Path::new("tests/ephemeral"));
assert!(result.is_err(), "profile path must reject symlink traversal: {result:?}");
if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_WALLET_DIRECTORY_INVALID);
}
}
cleanup_fixture(root.as_path());
cleanup_fixture(outside.as_path());
}
fn fixture_path(name: &str) -> std::path::PathBuf {
return std::env::temp_dir().join(std::format!("ksp-wallet-desk-{name}-{}", std::process::id()));
}
fn cleanup_fixture(path: &std::path::Path) {
let metadata = std::fs::symlink_metadata(path);
match metadata {
std::result::Result::Ok(metadata) if metadata.is_dir() && !metadata.file_type().is_symlink() => {
let _ = std::fs::remove_dir_all(path);
},
std::result::Result::Ok(_) => {
let _ = std::fs::remove_file(path);
},
std::result::Result::Err(_) => {},
}
}

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-config-lib/README.md --> <!-- file: crates/ksp-config-lib/README.md -->
<!-- version: 5 --> <!-- version: 6 -->
# ksp-config-lib # ksp-config-lib
@@ -33,18 +33,21 @@ La crate centralise les documents JSON, leurs schemas, les profils et compositio
Le registre par défaut connaît : Le registre par défaut connaît :
```text ```text
cfg.composite.ksp-app-wallet-desk -> config/composite.ksp-app-wallet-desk.json
cfg.std.logging -> config/std.logging.json cfg.std.logging -> config/std.logging.json
cfg.std.transport -> config/std.transport.json cfg.std.transport -> config/std.transport.json
cfg.std.wallet -> config/std.wallet.json
schema.composite -> config/schemas/composite.schema.json
schema.std.logging -> config/schemas/std.logging.schema.json schema.std.logging -> config/schemas/std.logging.schema.json
schema.std.transport -> config/schemas/std.transport.schema.json schema.std.transport -> config/schemas/std.transport.schema.json
schema.composite -> config/schemas/composite.schema.json schema.std.wallet -> config/schemas/std.wallet.schema.json
``` ```
`ConfigFileRegistry::descriptors()` expose ces descripteurs en lecture seule et dans un ordre déterministe par `file_id`. Une application de management peut ainsi découvrir les fichiers connus sans maintenir une liste parallèle ni dépendre de leurs filenames physiques. `ConfigFileRegistry::descriptors()` expose ces descripteurs en lecture seule et dans un ordre déterministe par `file_id`. Une application de management peut ainsi découvrir les fichiers connus sans maintenir une liste parallèle ni dépendre de leurs filenames physiques.
`ConfigManagement::read_source()` permet d'inspecter le texte brut d'un document Config enregistré même lorsque ce document est invalide. `save_source_candidate()` complète cette frontière : le candidat brut est parsé, validé contre son schema et les invariants sémantiques KSP, puis persisté atomiquement uniquement après validation complète. Le `file_id` doit appartenir au registre et désigner un document Config ; aucun path arbitraire n'est accepté. `ConfigManagement::read_source()` permet d'inspecter le texte brut d'un document Config enregistré même lorsque ce document est invalide. `save_source_candidate()` complète cette frontière : le candidat brut est parsé, validé contre son schema et les invariants sémantiques KSP, puis persisté atomiquement uniquement après validation complète. Le `file_id` doit appartenir au registre et désigner un document Config ; aucun path arbitraire n'est accepté.
`config/examples/composite.example.json` démontre le format composite sans créer de composite runtime fictif. `config/examples/composite.example.json` conserve lexemple générique. `config/composite.ksp-app-wallet-desk.json` est le premier composite runtime concret : il sélectionne Logging, Transport et Wallet par `file_id`, sans dépendre de leurs filenames physiques.
Le fichier local d'environnement est : Le fichier local d'environnement est :
@@ -62,9 +65,9 @@ Les autres crates et applications KSP ne doivent pas :
- parser ou écrire directement `.env` ; - parser ou écrire directement `.env` ;
- ouvrir directement les documents Config connus par leur filename physique ; - ouvrir directement les documents Config connus par leur filename physique ;
- réimplémenter la sélection de profils, les compositions ou les placeholders ; - réimplémenter la sélection de profils, les compositions ou les placeholders ;
- reconstruire elles-mêmes la configuration Logging ou Transport depuis le JSON. - reconstruire elles-mêmes la configuration Logging, Transport ou Wallet depuis le JSON.
`ksp-config-lib` dépend de `ksp-core-lib` pour `Error`/`Result`, de `ksp-logging-lib` pour les événements Config utiles et le contrat `LoggingSettings`, et de `ksp-onchain-transport-lib` pour construire le contrat runtime Transport dans la direction Config -> Transport. `ksp-config-lib` dépend de `ksp-core-lib` pour `Error`/`Result`, de `ksp-logging-lib` pour les événements Config utiles et le contrat `LoggingSettings`, et de `ksp-onchain-transport-lib` pour construire le contrat runtime Transport dans la direction Config -> Transport. Le document Wallet reste un contrat de chemins/profils Config et nintroduit aucune dépendance Config -> `ksp-wallet-lib`.
La dépendance inverse est interdite : `ksp-core-lib`, `ksp-logging-lib` et `ksp-onchain-transport-lib` ne dépendent pas de Config. La dépendance inverse est interdite : `ksp-core-lib`, `ksp-logging-lib` et `ksp-onchain-transport-lib` ne dépendent pas de Config.
@@ -78,7 +81,7 @@ Un secret reste accessible au runtime ou au management lorsqu'un consumer autori
Les méthodes `reveal_*` constituent un opt-in explicite au réel. L'authentification/autorisation de l'utilisateur humain appartient à l'application appelante et les valeurs retournées par ces méthodes ne doivent jamais être journalisées. Les méthodes `reveal_*` constituent un opt-in explicite au réel. L'authentification/autorisation de l'utilisateur humain appartient à l'application appelante et les valeurs retournées par ces méthodes ne doivent jamais être journalisées.
Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configuration effective. Le document Transport les accepte pour les URLs endpoint : la valeur réelle est transmise au runtime légitime, tandis que la projection sûre et les `Debug` restent redacted. Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configuration effective. Le document Transport les accepte pour les URLs endpoint : la valeur réelle est transmise au runtime légitime, tandis que la projection sûre et les `Debug` restent redacted. `std.wallet` refuse également toute sensibilité `Secret` pour `wallets_directory`/`wallets_subdirectory`; les passwords Wallet restent un autre flux Config et ne sont jamais stockés dans ce JSON.
## Documentation ## Documentation
@@ -87,4 +90,6 @@ Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configur
- [`../../docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md`](../../docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md) — plan historique détaillé de la fondation Config ; - [`../../docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md`](../../docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md) — plan historique détaillé de la fondation Config ;
- [`../../config/std.logging.json`](../../config/std.logging.json) — document standard Logging ; - [`../../config/std.logging.json`](../../config/std.logging.json) — document standard Logging ;
- [`../../config/std.transport.json`](../../config/std.transport.json) — document standard HTTP Transport ; - [`../../config/std.transport.json`](../../config/std.transport.json) — document standard HTTP Transport ;
- [`../../config/std.wallet.json`](../../config/std.wallet.json) — racine Wallet globale et sous-répertoire optionnel par profil ;
- [`../../config/composite.ksp-app-wallet-desk.json`](../../config/composite.ksp-app-wallet-desk.json) — composition Logging/Transport/Wallet de Wallet Desk ;
- [`../../.env.example`](../../.env.example) — inventaire versionné des variables d'environnement runtime. - [`../../.env.example`](../../.env.example) — inventaire versionné des variables d'environnement runtime.

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-config-lib/TODO.md --> <!-- file: crates/ksp-config-lib/TODO.md -->
<!-- version: 4 --> <!-- version: 5 -->
# TODO ksp-config-lib # TODO ksp-config-lib
@@ -35,6 +35,14 @@ Ces points ne nécessitent pas de duplication de logique dans `ksp-config-lib`;
`0.2.1-pre.006` introduit le premier nouveau domaine standard depuis Logging : `std.transport.json`, son schema, son exemple, son enregistrement et l'adapter Config -> `HttpTransportSettings`. Cette extension confirme que les nouveaux domaines restent ajoutés à la demande d'un consumer réel, sans transformer Config en propriétaire du runtime Transport. `0.2.1-pre.006` introduit le premier nouveau domaine standard depuis Logging : `std.transport.json`, son schema, son exemple, son enregistrement et l'adapter Config -> `HttpTransportSettings`. Cette extension confirme que les nouveaux domaines restent ajoutés à la demande d'un consumer réel, sans transformer Config en propriétaire du runtime Transport.
## Extension `0.2.6` — Wallet Desk
`0.2.6-pre.003` ajoute le domaine standard Wallet au moment où son premier consumer réel en a besoin : `cfg.std.wallet`, `schema.std.wallet`, `ResolvedWalletConfig` et le composite concret `cfg.composite.ksp-app-wallet-desk`. `wallets_directory` reste global ; `wallets_subdirectory` est optionnel par profil et ne peut pas sortir de la racine par syntaxe de path. Ladapter Config résout les paths et leur provenance mais ne crée pas de répertoire et ne dépend pas de `ksp-wallet-lib`.
Le même delta ajoute les entrées `resolve_*_config_profile` nécessaires pour mapper un profil Logging/Wallet déjà sélectionné par un composite sans perdre sa provenance `Composite`. La préparation filesystem du répertoire effectif appartient à `ksp-app-wallet-desk`.
Les futurs passwords `KSP_SECRET_WALLET_PASS_*` restent hors du JSON `std.wallet` et seront introduits dans la tranche dunlock dédiée ; Config restera leur propriétaire process/`.env`.
## Futur, uniquement au besoin ## Futur, uniquement au besoin
Les capacités suivantes sont différées jusqu'à l'apparition de composants réels : Les capacités suivantes sont différées jusqu'à l'apparition de composants réels :

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-config-lib/USAGE.md --> <!-- file: crates/ksp-config-lib/USAGE.md -->
<!-- version: 5 --> <!-- version: 6 -->
# Utilisation de ksp-config-lib # Utilisation de ksp-config-lib
@@ -28,8 +28,10 @@ Les arguments compris par Config sont :
```text ```text
--cfgpath=/path/to/config --cfgpath=/path/to/config
--schemapath=/path/to/schemas --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.logging=my-logging.json
--filemap=cfg.std.transport=my-transport.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. `cfgpath` et `schemapath` ne sont jamais lus depuis JSON, `.env` ou une variable KSP : cette règle évite un bootstrap récursif.
@@ -140,6 +142,26 @@ Les scalaires `*_ms` restent des valeurs Config et sont convertis en `std::time:
La dépendance reste unidirectionnelle : Config connaît le contrat Transport pour le construire ; Transport ne connaît ni Config, ni `.env`, ni les variables KSP. La dépendance reste unidirectionnelle : Config connaît le contrat Transport pour le 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` 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`.
## 5. Profils et composites ## 5. Profils et composites
Pour un document standard profilé : Pour un document standard profilé :
@@ -152,6 +174,19 @@ let profile = engine.load_resolved_profile(&file_id, std::option::Option::None);
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ë. 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` :
```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` ## 6. Management de `std.logging.json`
Une application de management construit la façade à partir d'un moteur : Une application de management construit la façade à partir d'un moteur :

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/document.rs // file: crates/ksp-config-lib/src/document.rs
// version: 6 // version: 7
/// A Config-managed JSON document that has passed syntax, schema and current semantic validation. /// A Config-managed JSON document that has passed syntax, schema and current semantic validation.
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
@@ -266,6 +266,10 @@ fn validate_document_semantics(engine: &ConfigDocumentEngine, document: &ConfigJ
if let std::result::Result::Err(error) = composite_validation { if let std::result::Result::Err(error) = composite_validation {
return std::result::Result::Err(error); return std::result::Result::Err(error);
} }
let wallet_validation = crate::validate_wallet_document_contract(document);
if let std::result::Result::Err(error) = wallet_validation {
return std::result::Result::Err(error);
}
if document.file_id().as_str() != crate::FILE_ID_STD_LOGGING { if document.file_id().as_str() != crate::FILE_ID_STD_LOGGING {
return std::result::Result::Ok(()); return std::result::Result::Ok(());
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/lib.rs // file: crates/ksp-config-lib/src/lib.rs
// version: 14 // version: 15
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -9,8 +9,8 @@
//! //!
//! The `0.1.3` surface owns bootstrap roots, the logical file registry, JSON/JSON Schema validation, standard-document profiles, generic composites and //! The `0.1.3` surface owns bootstrap roots, the logical file registry, JSON/JSON Schema validation, standard-document profiles, generic composites and
//! KSP/KSPB environment resolution through process + `.env` + fallback precedence. Resolved values preserve real/safe representations, sensitivity and //! KSP/KSPB environment resolution through process + `.env` + fallback precedence. Resolved values preserve real/safe representations, sensitivity and
//! provenance. Standard Logging and HTTP Transport documents map explicitly to their runtime settings contracts, while the management surface provides //! provenance. Standard Logging, HTTP Transport and Wallet documents map explicitly to their runtime consumer contracts, while the management surface
//! typed Logging mutation, safe environment reports, explicit privileged reveal calls and atomic JSON/`.env` persistence. //! provides typed Logging mutation, safe environment reports, explicit privileged reveal calls and atomic JSON/`.env` persistence.
mod bootstrap; mod bootstrap;
mod composite; mod composite;
@@ -25,6 +25,7 @@ mod profile;
mod registry; mod registry;
mod sensitivity; mod sensitivity;
mod transport; mod transport;
mod wallet;
/// Bootstrap argument used to replace the configuration document root. /// Bootstrap argument used to replace the configuration document root.
pub use self::bootstrap::ARG_CFG_PATH; pub use self::bootstrap::ARG_CFG_PATH;
@@ -138,6 +139,8 @@ pub use self::registry::ConfigFileId;
pub use self::registry::ConfigFileKind; pub use self::registry::ConfigFileKind;
/// Registry of KSP-known logical Config files and their replaceable physical filenames. /// Registry of KSP-known logical Config files and their replaceable physical filenames.
pub use self::registry::ConfigFileRegistry; pub use self::registry::ConfigFileRegistry;
/// Default physical filename for the Wallet Desk composite configuration document.
pub use self::registry::DEFAULT_COMPOSITE_KSP_APP_WALLET_DESK_FILENAME;
/// Default physical filename for the generic composite JSON Schema document. /// Default physical filename for the generic composite JSON Schema document.
pub use self::registry::DEFAULT_COMPOSITE_SCHEMA_FILENAME; pub use self::registry::DEFAULT_COMPOSITE_SCHEMA_FILENAME;
/// Default physical filename for the standard Logging configuration document. /// Default physical filename for the standard Logging configuration document.
@@ -148,16 +151,26 @@ pub use self::registry::DEFAULT_STD_LOGGING_SCHEMA_FILENAME;
pub use self::registry::DEFAULT_STD_TRANSPORT_FILENAME; pub use self::registry::DEFAULT_STD_TRANSPORT_FILENAME;
/// Default physical filename for the standard HTTP Transport JSON Schema document. /// Default physical filename for the standard HTTP Transport JSON Schema document.
pub use self::registry::DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME; pub use self::registry::DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME;
/// Default physical filename for the standard Wallet configuration document.
pub use self::registry::DEFAULT_STD_WALLET_FILENAME;
/// Default physical filename for the standard Wallet JSON Schema document.
pub use self::registry::DEFAULT_STD_WALLET_SCHEMA_FILENAME;
/// Logical file identifier for the Wallet Desk composite configuration document.
pub use self::registry::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK;
/// Logical file identifier for the generic composite JSON Schema document. /// Logical file identifier for the generic composite JSON Schema document.
pub use self::registry::FILE_ID_SCHEMA_COMPOSITE; pub use self::registry::FILE_ID_SCHEMA_COMPOSITE;
/// Logical file identifier for the standard Logging JSON Schema document. /// Logical file identifier for the standard Logging JSON Schema document.
pub use self::registry::FILE_ID_SCHEMA_STD_LOGGING; pub use self::registry::FILE_ID_SCHEMA_STD_LOGGING;
/// Logical file identifier for the standard HTTP Transport JSON Schema document. /// Logical file identifier for the standard HTTP Transport JSON Schema document.
pub use self::registry::FILE_ID_SCHEMA_STD_TRANSPORT; pub use self::registry::FILE_ID_SCHEMA_STD_TRANSPORT;
/// Logical file identifier for the standard Wallet JSON Schema document.
pub use self::registry::FILE_ID_SCHEMA_STD_WALLET;
/// Logical file identifier for the standard Logging configuration document. /// Logical file identifier for the standard Logging configuration document.
pub use self::registry::FILE_ID_STD_LOGGING; pub use self::registry::FILE_ID_STD_LOGGING;
/// Logical file identifier for the standard HTTP Transport configuration document. /// Logical file identifier for the standard HTTP Transport configuration document.
pub use self::registry::FILE_ID_STD_TRANSPORT; pub use self::registry::FILE_ID_STD_TRANSPORT;
/// Logical file identifier for the standard Wallet configuration document.
pub use self::registry::FILE_ID_STD_WALLET;
/// Sensitivity assigned to one Config value after environment resolution. /// Sensitivity assigned to one Config value after environment resolution.
pub use self::sensitivity::ConfigSensitivity; pub use self::sensitivity::ConfigSensitivity;
/// Provenance segment participating in one resolved Config value. /// Provenance segment participating in one resolved Config value.
@@ -170,6 +183,8 @@ pub use self::sensitivity::ResolvedConfigJson;
pub use self::sensitivity::ResolvedConfigText; pub use self::sensitivity::ResolvedConfigText;
/// Effective standard HTTP Transport configuration mapped to `ksp_onchain_transport_lib::HttpTransportSettings`. /// Effective standard HTTP Transport configuration mapped to `ksp_onchain_transport_lib::HttpTransportSettings`.
pub use self::transport::ResolvedTransportConfig; pub use self::transport::ResolvedTransportConfig;
/// Effective standard Wallet configuration resolved to validated filesystem roots.
pub use self::wallet::ResolvedWalletConfig;
/// Validates composite document contract. /// Validates composite document contract.
pub(crate) use self::composite::validate_composite_document_contract; pub(crate) use self::composite::validate_composite_document_contract;
@@ -189,3 +204,5 @@ pub(crate) use self::profile::load_resolved_profile_with_source;
pub(crate) use self::profile::validate_document_profile_contract; pub(crate) use self::profile::validate_document_profile_contract;
/// Executes the crate-internal build registry operation for the owning module. /// Executes the crate-internal build registry operation for the owning module.
pub(crate) use self::registry::build_registry; pub(crate) use self::registry::build_registry;
/// Validates standard Wallet source path invariants before environment resolution.
pub(crate) use self::wallet::validate_wallet_document_contract;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/logging.rs // file: crates/ksp-config-lib/src/logging.rs
// version: 1 // version: 2
/// Effective standard Logging configuration resolved from Config and mapped to the Logging runtime contract. /// Effective standard Logging configuration resolved from Config and mapped to the Logging runtime contract.
#[derive(Clone, Eq, PartialEq)] #[derive(Clone, Eq, PartialEq)]
@@ -103,6 +103,24 @@ impl crate::ConfigDocumentEngine {
}; };
return resolve_logging_profile(&profile, environment); return resolve_logging_profile(&profile, environment);
} }
/// Maps an already resolved standard Logging profile to the runtime Logging adapter while preserving its selection provenance.
///
/// This entry point is intended for profiles selected by a composite. The profile must reference `cfg.std.logging`.
pub fn resolve_logging_config_profile(
&self,
profile: &crate::ResolvedConfigProfile,
environment: &crate::ConfigEnvironment,
) -> ksp_core_lib::Result<ResolvedLoggingConfig> {
if profile.file_id().as_str() != crate::FILE_ID_STD_LOGGING {
return std::result::Result::Err(effective_error(profile, "resolved Config profile does not reference the standard Logging document"));
}
let descriptor = self.registry().descriptor(profile.file_id());
if let std::result::Result::Err(error) = descriptor {
return std::result::Result::Err(error);
}
return resolve_logging_profile(profile, environment);
}
} }
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]

View File

@@ -1,8 +1,10 @@
// file: crates/ksp-config-lib/src/registry.rs // file: crates/ksp-config-lib/src/registry.rs
// version: 7 // version: 8
/// Bootstrap argument used to replace a known Config filename mapping. /// Bootstrap argument used to replace a known Config filename mapping.
pub const ARG_FILE_MAP: &str = "--filemap"; pub const ARG_FILE_MAP: &str = "--filemap";
/// Default physical filename for the Wallet Desk composite configuration document.
pub const DEFAULT_COMPOSITE_KSP_APP_WALLET_DESK_FILENAME: &str = "composite.ksp-app-wallet-desk.json";
/// Default physical filename for the generic composite JSON Schema document. /// Default physical filename for the generic composite JSON Schema document.
pub const DEFAULT_COMPOSITE_SCHEMA_FILENAME: &str = "composite.schema.json"; pub const DEFAULT_COMPOSITE_SCHEMA_FILENAME: &str = "composite.schema.json";
/// Default physical filename for the standard Logging configuration document. /// Default physical filename for the standard Logging configuration document.
@@ -13,16 +15,26 @@ pub const DEFAULT_STD_LOGGING_SCHEMA_FILENAME: &str = "std.logging.schema.json";
pub const DEFAULT_STD_TRANSPORT_FILENAME: &str = "std.transport.json"; pub const DEFAULT_STD_TRANSPORT_FILENAME: &str = "std.transport.json";
/// Default physical filename for the standard HTTP Transport JSON Schema document. /// Default physical filename for the standard HTTP Transport JSON Schema document.
pub const DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME: &str = "std.transport.schema.json"; pub const DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME: &str = "std.transport.schema.json";
/// Default physical filename for the standard Wallet configuration document.
pub const DEFAULT_STD_WALLET_FILENAME: &str = "std.wallet.json";
/// Default physical filename for the standard Wallet JSON Schema document.
pub const DEFAULT_STD_WALLET_SCHEMA_FILENAME: &str = "std.wallet.schema.json";
/// Logical file identifier for the Wallet Desk composite configuration document.
pub const FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK: &str = "cfg.composite.ksp-app-wallet-desk";
/// Logical file identifier for the generic composite JSON Schema document. /// Logical file identifier for the generic composite JSON Schema document.
pub const FILE_ID_SCHEMA_COMPOSITE: &str = "schema.composite"; pub const FILE_ID_SCHEMA_COMPOSITE: &str = "schema.composite";
/// Logical file identifier for the standard Logging JSON Schema document. /// Logical file identifier for the standard Logging JSON Schema document.
pub const FILE_ID_SCHEMA_STD_LOGGING: &str = "schema.std.logging"; pub const FILE_ID_SCHEMA_STD_LOGGING: &str = "schema.std.logging";
/// Logical file identifier for the standard HTTP Transport JSON Schema document. /// Logical file identifier for the standard HTTP Transport JSON Schema document.
pub const FILE_ID_SCHEMA_STD_TRANSPORT: &str = "schema.std.transport"; pub const FILE_ID_SCHEMA_STD_TRANSPORT: &str = "schema.std.transport";
/// Logical file identifier for the standard Wallet JSON Schema document.
pub const FILE_ID_SCHEMA_STD_WALLET: &str = "schema.std.wallet";
/// Logical file identifier for the standard Logging configuration document. /// Logical file identifier for the standard Logging configuration document.
pub const FILE_ID_STD_LOGGING: &str = "cfg.std.logging"; pub const FILE_ID_STD_LOGGING: &str = "cfg.std.logging";
/// Logical file identifier for the standard HTTP Transport configuration document. /// Logical file identifier for the standard HTTP Transport configuration document.
pub const FILE_ID_STD_TRANSPORT: &str = "cfg.std.transport"; pub const FILE_ID_STD_TRANSPORT: &str = "cfg.std.transport";
/// Logical file identifier for the standard Wallet configuration document.
pub const FILE_ID_STD_WALLET: &str = "cfg.std.wallet";
/// Stable logical identifier for a Config-managed file. /// Stable logical identifier for a Config-managed file.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
@@ -128,6 +140,22 @@ pub struct ConfigFileRegistry {
impl ConfigFileRegistry { impl ConfigFileRegistry {
/// Creates the registry containing the KSP default file mappings known in the current release. /// Creates the registry containing the KSP default file mappings known in the current release.
pub fn defaults() -> ksp_core_lib::Result<Self> { pub fn defaults() -> ksp_core_lib::Result<Self> {
let composite = ConfigFileDescriptor::new(
FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK,
ConfigFileKind::Config,
DEFAULT_COMPOSITE_KSP_APP_WALLET_DESK_FILENAME,
std::option::Option::Some(FILE_ID_SCHEMA_COMPOSITE),
);
let composite = match composite {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let composite_schema =
ConfigFileDescriptor::new(FILE_ID_SCHEMA_COMPOSITE, ConfigFileKind::Schema, DEFAULT_COMPOSITE_SCHEMA_FILENAME, std::option::Option::None);
let composite_schema = match composite_schema {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let logging = ConfigFileDescriptor::new( let logging = ConfigFileDescriptor::new(
FILE_ID_STD_LOGGING, FILE_ID_STD_LOGGING,
ConfigFileKind::Config, ConfigFileKind::Config,
@@ -160,13 +188,23 @@ impl ConfigFileRegistry {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
}; };
let composite_schema = let wallet = ConfigFileDescriptor::new(
ConfigFileDescriptor::new(FILE_ID_SCHEMA_COMPOSITE, ConfigFileKind::Schema, DEFAULT_COMPOSITE_SCHEMA_FILENAME, std::option::Option::None); FILE_ID_STD_WALLET,
let composite_schema = match composite_schema { ConfigFileKind::Config,
DEFAULT_STD_WALLET_FILENAME,
std::option::Option::Some(FILE_ID_SCHEMA_STD_WALLET),
);
let wallet = match wallet {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
}; };
return crate::build_registry([logging, logging_schema, transport, transport_schema, composite_schema]); let wallet_schema =
ConfigFileDescriptor::new(FILE_ID_SCHEMA_STD_WALLET, ConfigFileKind::Schema, DEFAULT_STD_WALLET_SCHEMA_FILENAME, std::option::Option::None);
let wallet_schema = match wallet_schema {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::build_registry([composite, composite_schema, logging, logging_schema, transport, transport_schema, wallet, wallet_schema]);
} }
/// Creates the default registry and applies repeatable `--filemap=<file_id>=<filename>` overrides from raw process arguments. /// Creates the default registry and applies repeatable `--filemap=<file_id>=<filename>` overrides from raw process arguments.

View File

@@ -0,0 +1,290 @@
// file: crates/ksp-config-lib/src/wallet.rs
// version: 1
/// Effective standard Wallet configuration resolved from Config.
#[derive(Clone, Eq, PartialEq)]
pub struct ResolvedWalletConfig {
effective: crate::ResolvedConfigJson,
effective_wallets_directory: std::path::PathBuf,
file_id: crate::ConfigFileId,
profile_id: String,
selection_source: crate::ConfigProfileSelectionSource,
source_path: std::path::PathBuf,
wallets_directory: std::path::PathBuf,
wallets_subdirectory: std::option::Option<std::path::PathBuf>,
}
impl ResolvedWalletConfig {
/// Returns the detailed environment-resolved effective Config view.
#[must_use]
pub const fn effective(&self) -> &crate::ResolvedConfigJson {
return &self.effective;
}
/// Returns the effective Wallet directory after applying the optional profile subdirectory.
#[must_use]
pub fn effective_wallets_directory(&self) -> &std::path::Path {
return self.effective_wallets_directory.as_path();
}
/// Returns the logical Config document identifier used by this runtime configuration.
#[must_use]
pub const fn file_id(&self) -> &crate::ConfigFileId {
return &self.file_id;
}
/// Returns the selected standard Wallet profile identifier.
#[must_use]
pub fn profile_id(&self) -> &str {
return self.profile_id.as_str();
}
/// Returns the source that selected the standard Wallet profile.
#[must_use]
pub const fn selection_source(&self) -> crate::ConfigProfileSelectionSource {
return self.selection_source;
}
/// Returns the physical source Config document path.
#[must_use]
pub fn source_path(&self) -> &std::path::Path {
return self.source_path.as_path();
}
/// Returns the resolved global Wallet root directory.
#[must_use]
pub fn wallets_directory(&self) -> &std::path::Path {
return self.wallets_directory.as_path();
}
/// Returns the validated optional relative Wallet subdirectory selected by the profile.
#[must_use]
pub fn wallets_subdirectory(&self) -> std::option::Option<&std::path::Path> {
return self.wallets_subdirectory.as_deref();
}
}
impl std::fmt::Debug for ResolvedWalletConfig {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("ResolvedWalletConfig")
.field("effective", &self.effective)
.field("effective_wallets_directory", &self.effective_wallets_directory)
.field("file_id", &self.file_id)
.field("profile_id", &self.profile_id)
.field("selection_source", &self.selection_source)
.field("source_path", &self.source_path)
.field("wallets_directory", &self.wallets_directory)
.field("wallets_subdirectory", &self.wallets_subdirectory)
.finish();
}
}
impl crate::ConfigDocumentEngine {
/// Loads the standard Wallet document, selects a profile, resolves environment placeholders and validates the effective directory contract.
///
/// `requested_profile = None` uses the document `default_profile`; `Some(profile_id)` requests an explicit profile. `wallets_directory` may be absolute
/// or relative to the process current working directory. `wallets_subdirectory` is always relative and may contain nested normal path components only.
pub fn load_resolved_wallet_config(
&self,
requested_profile: std::option::Option<&str>,
environment: &crate::ConfigEnvironment,
) -> ksp_core_lib::Result<ResolvedWalletConfig> {
let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_WALLET);
let file_id = match file_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let profile = self.load_resolved_profile(&file_id, requested_profile);
let profile = match profile {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return resolve_wallet_profile(&profile, environment);
}
/// Maps an already resolved standard Wallet profile to the runtime Wallet Config adapter.
///
/// This entry point is intended for profiles selected by a composite and preserves their original selection source instead of reclassifying the profile as
/// an explicit selection. The profile must reference `cfg.std.wallet`.
pub fn resolve_wallet_config_profile(
&self,
profile: &crate::ResolvedConfigProfile,
environment: &crate::ConfigEnvironment,
) -> ksp_core_lib::Result<ResolvedWalletConfig> {
if profile.file_id().as_str() != crate::FILE_ID_STD_WALLET {
return std::result::Result::Err(effective_error(profile, "resolved Config profile does not reference the standard Wallet document"));
}
let descriptor = self.registry().descriptor(profile.file_id());
if let std::result::Result::Err(error) = descriptor {
return std::result::Result::Err(error);
}
return resolve_wallet_profile(profile, environment);
}
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectiveWalletSource {
format_version: u32,
profile_id: String,
wallets_directory: String,
wallets_subdirectory: std::option::Option<String>,
}
/// Validates standard Wallet source path invariants that are meaningful before environment resolution.
pub(crate) fn validate_wallet_document_contract(document: &crate::ConfigJsonDocument) -> ksp_core_lib::Result<()> {
if document.file_id().as_str() != crate::FILE_ID_STD_WALLET {
return std::result::Result::Ok(());
}
let profiles = document.value().get("profiles").and_then(serde_json::Value::as_array);
let profiles = match profiles {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Ok(()),
};
for (profile_index, profile) in profiles.iter().enumerate() {
let subdirectory = profile.get("wallets_subdirectory").and_then(serde_json::Value::as_str);
if let std::option::Option::Some(value) = subdirectory {
let validation = validate_relative_subdirectory(value);
if let std::result::Result::Err(_) = validation {
return std::result::Result::Err(
wallet_document_error(document, "wallets_subdirectory must contain only relative normal path components")
.with_context("profile_index", profile_index.to_string()),
);
}
}
}
return std::result::Result::Ok(());
}
fn resolve_wallet_profile(profile: &crate::ResolvedConfigProfile, environment: &crate::ConfigEnvironment) -> ksp_core_lib::Result<ResolvedWalletConfig> {
let effective = profile.resolve_effective_environment_detailed(environment);
let effective = match effective {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if effective.sensitivity().is_secret() {
return std::result::Result::Err(effective_error(profile, "standard Wallet configuration must not consume Secret environment values"));
}
let source = serde_json::from_value::<EffectiveWalletSource>(effective.value().clone());
let source = match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
effective_error(profile, "effective Wallet Config cannot be decoded into the runtime adapter contract").with_source(error),
);
},
};
if source.format_version != 1 {
return std::result::Result::Err(effective_error(profile, "effective Wallet format_version is unsupported"));
}
if source.profile_id != profile.profile_id() {
return std::result::Result::Err(effective_error(profile, "effective Wallet profile_id does not match the selected profile"));
}
let wallets_directory = resolve_wallets_directory(source.wallets_directory.as_str(), profile);
let wallets_directory = match wallets_directory {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let wallets_subdirectory = resolve_wallets_subdirectory(source.wallets_subdirectory.as_deref(), profile);
let wallets_subdirectory = match wallets_subdirectory {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let effective_wallets_directory = match &wallets_subdirectory {
std::option::Option::Some(value) => wallets_directory.join(value),
std::option::Option::None => wallets_directory.clone(),
};
return std::result::Result::Ok(ResolvedWalletConfig {
effective,
effective_wallets_directory,
file_id: profile.file_id().clone(),
profile_id: profile.profile_id().to_owned(),
selection_source: profile.selection_source(),
source_path: profile.path().to_path_buf(),
wallets_directory,
wallets_subdirectory,
});
}
fn resolve_wallets_directory(value: &str, profile: &crate::ResolvedConfigProfile) -> ksp_core_lib::Result<std::path::PathBuf> {
if value.trim().is_empty() {
return std::result::Result::Err(effective_field_error(profile, "wallets_directory", "effective Wallet wallets_directory must not be empty"));
}
let path = std::path::PathBuf::from(value);
if path.is_absolute() {
return std::result::Result::Ok(path);
}
let current_directory = std::env::current_dir();
return match current_directory {
std::result::Result::Ok(current_directory) => std::result::Result::Ok(current_directory.join(path)),
std::result::Result::Err(error) => std::result::Result::Err(
effective_field_error(profile, "wallets_directory", "process current working directory cannot be resolved").with_source(error),
),
};
}
fn resolve_wallets_subdirectory(
value: std::option::Option<&str>,
profile: &crate::ResolvedConfigProfile,
) -> ksp_core_lib::Result<std::option::Option<std::path::PathBuf>> {
let value = match value {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
};
let validation = validate_relative_subdirectory(value);
if let std::result::Result::Err(()) = validation {
return std::result::Result::Err(effective_field_error(
profile,
"wallets_subdirectory",
"effective Wallet wallets_subdirectory must contain only relative normal path components",
));
}
return std::result::Result::Ok(std::option::Option::Some(std::path::PathBuf::from(value)));
}
fn validate_relative_subdirectory(value: &str) -> std::result::Result<(), ()> {
if value.trim().is_empty() {
return std::result::Result::Err(());
}
let path = std::path::Path::new(value);
if path.is_absolute() {
return std::result::Result::Err(());
}
let mut component_count: usize = 0;
for component in path.components() {
match component {
std::path::Component::Normal(_) => component_count += 1,
std::path::Component::CurDir | std::path::Component::ParentDir | std::path::Component::RootDir | std::path::Component::Prefix(_) => {
return std::result::Result::Err(());
},
}
}
if component_count == 0 {
return std::result::Result::Err(());
}
return std::result::Result::Ok(());
}
fn wallet_document_error(document: &crate::ConfigJsonDocument, reason: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID, "standard Wallet Config violates KSP semantic invariants")
.with_context("file_id", document.file_id().as_str())
.with_context("path", document.path().to_string_lossy().into_owned())
.with_context("reason", reason);
}
fn effective_field_error(profile: &crate::ResolvedConfigProfile, field: &'static str, reason: &'static str) -> ksp_core_lib::Error {
return effective_error(profile, reason).with_context("field", field);
}
fn effective_error(profile: &crate::ResolvedConfigProfile, reason: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID, "effective Config cannot be mapped to the requested runtime contract")
.with_context("file_id", profile.file_id().as_str())
.with_context("path", profile.path().to_string_lossy().into_owned())
.with_context("profile_id", profile.profile_id())
.with_context("reason", reason);
}
#[cfg(test)]
#[path = "../unit_tests/wallet.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/tests/ownership.rs // file: crates/ksp-config-lib/tests/ownership.rs
// version: 3 // version: 4
//! Workspace ownership audits for KSP application configuration boundaries. //! Workspace ownership audits for KSP application configuration boundaries.
@@ -241,11 +241,14 @@ fn workspace_crates_do_not_hardcode_config_managed_physical_files() {
}; };
for token in [ for token in [
"\".env\"", "\".env\"",
"\"composite.ksp-app-wallet-desk.json\"",
"\"composite.schema.json\"",
"\"std.logging.json\"", "\"std.logging.json\"",
"\"std.logging.schema.json\"", "\"std.logging.schema.json\"",
"\"std.transport.json\"", "\"std.transport.json\"",
"\"std.transport.schema.json\"", "\"std.transport.schema.json\"",
"\"composite.schema.json\"", "\"std.wallet.json\"",
"\"std.wallet.schema.json\"",
] { ] {
assert!(!source.contains(token), "{} hardcodes Config-managed physical resource {token}; use ksp-config-lib contracts", rust_file.display()); assert!(!source.contains(token), "{} hardcodes Config-managed physical resource {token}; use ksp-config-lib contracts", rust_file.display());
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/tests/public_api.rs // file: crates/ksp-config-lib/tests/public_api.rs
// version: 17 // version: 19
//! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity, //! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity,
//! Logging/Transport adapters and management contracts. //! Logging/Transport adapters and management contracts.
@@ -80,18 +80,19 @@ fn registry_descriptor_inventory_is_available_from_crate_root() {
assert!(registry.is_ok(), "public registry should remain constructible: {registry:?}"); assert!(registry.is_ok(), "public registry should remain constructible: {registry:?}");
if let std::result::Result::Ok(registry) = registry { if let std::result::Result::Ok(registry) = registry {
let descriptors: std::vec::Vec<&ksp_config_lib::ConfigFileDescriptor> = registry.descriptors().collect(); let descriptors: std::vec::Vec<&ksp_config_lib::ConfigFileDescriptor> = registry.descriptors().collect();
assert_eq!(descriptors.len(), 5); assert_eq!(descriptors.len(), 8);
assert_eq!(descriptors[0].file_id().as_str(), ksp_config_lib::FILE_ID_STD_LOGGING); assert_eq!(descriptors[0].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK);
assert_eq!(descriptors[0].kind(), ksp_config_lib::ConfigFileKind::Config); assert_eq!(descriptors[1].file_id().as_str(), ksp_config_lib::FILE_ID_STD_LOGGING);
assert_eq!(descriptors[1].file_id().as_str(), ksp_config_lib::FILE_ID_STD_TRANSPORT); assert_eq!(descriptors[2].file_id().as_str(), ksp_config_lib::FILE_ID_STD_TRANSPORT);
assert_eq!(descriptors[1].kind(), ksp_config_lib::ConfigFileKind::Config); assert_eq!(descriptors[3].file_id().as_str(), ksp_config_lib::FILE_ID_STD_WALLET);
assert_eq!(descriptors[2].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_COMPOSITE); assert_eq!(descriptors[4].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_COMPOSITE);
assert_eq!(descriptors[3].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_LOGGING); assert_eq!(descriptors[5].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_LOGGING);
assert_eq!(descriptors[4].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT); assert_eq!(descriptors[6].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT);
let schema_file_id = descriptors[1].schema_file_id(); assert_eq!(descriptors[7].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_WALLET);
assert!(schema_file_id.is_some(), "public Transport descriptor should preserve schema association"); let schema_file_id = descriptors[3].schema_file_id();
assert!(schema_file_id.is_some(), "public Wallet descriptor should preserve schema association");
if let std::option::Option::Some(schema_file_id) = schema_file_id { if let std::option::Option::Some(schema_file_id) = schema_file_id {
assert_eq!(schema_file_id.as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT); assert_eq!(schema_file_id.as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_WALLET);
} }
} }
} }
@@ -198,7 +199,8 @@ fn sensitivity_and_safe_resolution_contracts_are_available_from_crate_root() {
#[test] #[test]
fn logging_adapter_contract_is_available_from_crate_root() { fn logging_adapter_contract_is_available_from_crate_root() {
let adapter = ksp_config_lib::ConfigDocumentEngine::load_resolved_logging_config; let adapter = ksp_config_lib::ConfigDocumentEngine::load_resolved_logging_config;
let _ = adapter; let composite_adapter = ksp_config_lib::ConfigDocumentEngine::resolve_logging_config_profile;
let _ = (adapter, composite_adapter);
assert_eq!(ksp_config_lib::ERROR_CODE_EFFECTIVE_CONFIG_INVALID.domain(), "config"); assert_eq!(ksp_config_lib::ERROR_CODE_EFFECTIVE_CONFIG_INVALID.domain(), "config");
assert_eq!(ksp_config_lib::ERROR_CODE_EFFECTIVE_CONFIG_INVALID.code(), "effective_config_invalid"); assert_eq!(ksp_config_lib::ERROR_CODE_EFFECTIVE_CONFIG_INVALID.code(), "effective_config_invalid");
assert!(std::mem::size_of::<ksp_config_lib::ResolvedLoggingConfig>() > 0); assert!(std::mem::size_of::<ksp_config_lib::ResolvedLoggingConfig>() > 0);
@@ -254,3 +256,16 @@ fn transport_adapter_contract_is_available_from_crate_root() {
assert_eq!(ksp_config_lib::DEFAULT_STD_TRANSPORT_FILENAME, "std.transport.json"); assert_eq!(ksp_config_lib::DEFAULT_STD_TRANSPORT_FILENAME, "std.transport.json");
assert_eq!(ksp_config_lib::DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME, "std.transport.schema.json"); assert_eq!(ksp_config_lib::DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME, "std.transport.schema.json");
} }
#[test]
fn wallet_adapter_contract_is_available_from_crate_root() {
let _loader = ksp_config_lib::ConfigDocumentEngine::load_resolved_wallet_config;
let _composite_loader = ksp_config_lib::ConfigDocumentEngine::resolve_wallet_config_profile;
assert!(std::mem::size_of::<ksp_config_lib::ResolvedWalletConfig>() > 0);
assert_eq!(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK, "cfg.composite.ksp-app-wallet-desk");
assert_eq!(ksp_config_lib::FILE_ID_STD_WALLET, "cfg.std.wallet");
assert_eq!(ksp_config_lib::FILE_ID_SCHEMA_STD_WALLET, "schema.std.wallet");
assert_eq!(ksp_config_lib::DEFAULT_COMPOSITE_KSP_APP_WALLET_DESK_FILENAME, "composite.ksp-app-wallet-desk.json");
assert_eq!(ksp_config_lib::DEFAULT_STD_WALLET_FILENAME, "std.wallet.json");
assert_eq!(ksp_config_lib::DEFAULT_STD_WALLET_SCHEMA_FILENAME, "std.wallet.schema.json");
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/unit_tests/registry.rs // file: crates/ksp-config-lib/unit_tests/registry.rs
// version: 6 // version: 7
#[test] #[test]
fn descriptors_expose_complete_registry_in_deterministic_file_id_order() { fn descriptors_expose_complete_registry_in_deterministic_file_id_order() {
@@ -7,29 +7,21 @@ fn descriptors_expose_complete_registry_in_deterministic_file_id_order() {
assert!(registry.is_ok(), "default registry should be valid: {registry:?}"); assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
if let std::result::Result::Ok(registry) = registry { if let std::result::Result::Ok(registry) = registry {
let descriptors: std::vec::Vec<&crate::ConfigFileDescriptor> = registry.descriptors().collect(); let descriptors: std::vec::Vec<&crate::ConfigFileDescriptor> = registry.descriptors().collect();
assert_eq!(descriptors.len(), 5); assert_eq!(descriptors.len(), 8);
assert_eq!(descriptors[0].file_id().as_str(), crate::FILE_ID_STD_LOGGING); assert_eq!(descriptors[0].file_id().as_str(), crate::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK);
assert_eq!(descriptors[0].kind(), crate::ConfigFileKind::Config); assert_eq!(descriptors[0].filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_KSP_APP_WALLET_DESK_FILENAME));
assert_eq!(descriptors[0].filename(), std::path::Path::new(crate::DEFAULT_STD_LOGGING_FILENAME)); assert_eq!(descriptors[0].schema_file_id().map(crate::ConfigFileId::as_str), std::option::Option::Some(crate::FILE_ID_SCHEMA_COMPOSITE));
let logging_schema_file_id = descriptors[0].schema_file_id(); assert_eq!(descriptors[1].file_id().as_str(), crate::FILE_ID_STD_LOGGING);
assert!(logging_schema_file_id.is_some(), "logging descriptor should expose its validation schema"); assert_eq!(descriptors[2].file_id().as_str(), crate::FILE_ID_STD_TRANSPORT);
if let std::option::Option::Some(schema_file_id) = logging_schema_file_id { assert_eq!(descriptors[3].file_id().as_str(), crate::FILE_ID_STD_WALLET);
assert_eq!(schema_file_id.as_str(), crate::FILE_ID_SCHEMA_STD_LOGGING); assert_eq!(descriptors[3].filename(), std::path::Path::new(crate::DEFAULT_STD_WALLET_FILENAME));
} assert_eq!(descriptors[3].schema_file_id().map(crate::ConfigFileId::as_str), std::option::Option::Some(crate::FILE_ID_SCHEMA_STD_WALLET));
assert_eq!(descriptors[1].file_id().as_str(), crate::FILE_ID_STD_TRANSPORT); assert_eq!(descriptors[4].file_id().as_str(), crate::FILE_ID_SCHEMA_COMPOSITE);
assert_eq!(descriptors[1].kind(), crate::ConfigFileKind::Config); assert_eq!(descriptors[5].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_LOGGING);
assert_eq!(descriptors[1].filename(), std::path::Path::new(crate::DEFAULT_STD_TRANSPORT_FILENAME)); assert_eq!(descriptors[6].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_TRANSPORT);
let transport_schema_file_id = descriptors[1].schema_file_id(); assert_eq!(descriptors[7].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_WALLET);
assert!(transport_schema_file_id.is_some(), "transport descriptor should expose its validation schema"); assert!(descriptors[0..4].iter().all(|descriptor| return descriptor.kind() == crate::ConfigFileKind::Config));
if let std::option::Option::Some(schema_file_id) = transport_schema_file_id { assert!(descriptors[4..8].iter().all(|descriptor| return descriptor.kind() == crate::ConfigFileKind::Schema));
assert_eq!(schema_file_id.as_str(), crate::FILE_ID_SCHEMA_STD_TRANSPORT);
}
assert_eq!(descriptors[2].file_id().as_str(), crate::FILE_ID_SCHEMA_COMPOSITE);
assert_eq!(descriptors[2].kind(), crate::ConfigFileKind::Schema);
assert_eq!(descriptors[3].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_LOGGING);
assert_eq!(descriptors[3].kind(), crate::ConfigFileKind::Schema);
assert_eq!(descriptors[4].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_TRANSPORT);
assert_eq!(descriptors[4].kind(), crate::ConfigFileKind::Schema);
} }
} }
@@ -118,6 +110,39 @@ fn defaults_register_transport_document_and_schema_with_distinct_roots() {
} }
} }
#[test]
fn defaults_register_wallet_document_schema_and_wallet_desk_composite() {
let registry = crate::ConfigFileRegistry::defaults();
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
if let std::result::Result::Ok(registry) = registry {
let wallet_id = crate::ConfigFileId::new(crate::FILE_ID_STD_WALLET);
let wallet_schema_id = crate::ConfigFileId::new(crate::FILE_ID_SCHEMA_STD_WALLET);
let composite_id = crate::ConfigFileId::new(crate::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK);
assert!(wallet_id.is_ok());
assert!(wallet_schema_id.is_ok());
assert!(composite_id.is_ok());
if let (std::result::Result::Ok(wallet_id), std::result::Result::Ok(wallet_schema_id), std::result::Result::Ok(composite_id)) =
(wallet_id, wallet_schema_id, composite_id)
{
let wallet = registry.descriptor(&wallet_id);
let wallet_schema = registry.descriptor(&wallet_schema_id);
let composite = registry.descriptor(&composite_id);
assert!(wallet.is_ok());
assert!(wallet_schema.is_ok());
assert!(composite.is_ok());
if let (std::result::Result::Ok(wallet), std::result::Result::Ok(wallet_schema), std::result::Result::Ok(composite)) =
(wallet, wallet_schema, composite)
{
assert_eq!(wallet.filename(), std::path::Path::new(crate::DEFAULT_STD_WALLET_FILENAME));
assert_eq!(wallet.schema_file_id(), std::option::Option::Some(&wallet_schema_id));
assert_eq!(wallet_schema.filename(), std::path::Path::new(crate::DEFAULT_STD_WALLET_SCHEMA_FILENAME));
assert_eq!(composite.filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_KSP_APP_WALLET_DESK_FILENAME));
assert_eq!(composite.schema_file_id().map(crate::ConfigFileId::as_str), std::option::Option::Some(crate::FILE_ID_SCHEMA_COMPOSITE));
}
}
}
}
#[test] #[test]
fn resolve_path_uses_descriptor_kind_to_select_bootstrap_root() { fn resolve_path_uses_descriptor_kind_to_select_bootstrap_root() {
let registry = crate::ConfigFileRegistry::defaults(); let registry = crate::ConfigFileRegistry::defaults();

View File

@@ -0,0 +1,200 @@
// file: crates/ksp-config-lib/unit_tests/wallet.rs
// version: 1
#[test]
fn default_wallet_config_resolves_global_root_without_subdirectory() {
let engine = workspace_engine();
assert!(engine.is_ok(), "workspace engine should be constructible: {engine:?}");
if let std::result::Result::Ok(engine) = engine {
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
let resolved = engine.load_resolved_wallet_config(std::option::Option::Some("default"), &environment);
assert!(resolved.is_ok(), "default Wallet profile should resolve: {resolved:?}");
if let std::result::Result::Ok(resolved) = resolved {
assert_eq!(resolved.file_id().as_str(), crate::FILE_ID_STD_WALLET);
assert_eq!(resolved.profile_id(), "default");
assert!(resolved.wallets_subdirectory().is_none());
assert!(resolved.wallets_directory().is_absolute());
assert_eq!(resolved.effective_wallets_directory(), resolved.wallets_directory());
}
}
}
#[test]
fn wallet_profile_appends_relative_subdirectory() {
let engine = workspace_engine();
assert!(engine.is_ok(), "workspace engine should be constructible: {engine:?}");
if let std::result::Result::Ok(engine) = engine {
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
let resolved = engine.load_resolved_wallet_config(std::option::Option::Some("tests"), &environment);
assert!(resolved.is_ok(), "tests Wallet profile should resolve: {resolved:?}");
if let std::result::Result::Ok(resolved) = resolved {
assert_eq!(resolved.wallets_subdirectory(), std::option::Option::Some(std::path::Path::new("tests")));
let expected = resolved.wallets_directory().join("tests");
assert_eq!(resolved.effective_wallets_directory(), expected.as_path());
}
}
}
#[test]
fn wallet_directory_environment_override_uses_config_precedence() {
let engine = workspace_engine();
assert!(engine.is_ok(), "workspace engine should be constructible: {engine:?}");
if let std::result::Result::Ok(engine) = engine {
let mut process = std::collections::BTreeMap::<String, String>::new();
process.insert("KSP_WALLETS_DIRECTORY".to_owned(), "process-wallets".to_owned());
let mut dotenv = std::collections::BTreeMap::<String, String>::new();
dotenv.insert("KSP_WALLETS_DIRECTORY".to_owned(), "dotenv-wallets".to_owned());
let environment = crate::ConfigEnvironment::from_maps(process, dotenv);
let resolved = engine.load_resolved_wallet_config(std::option::Option::Some("default"), &environment);
assert!(resolved.is_ok(), "Wallet environment override should resolve: {resolved:?}");
if let std::result::Result::Ok(resolved) = resolved {
assert!(resolved.wallets_directory().ends_with("process-wallets"));
assert!(resolved.effective().provenance_at("/wallets_directory").is_some());
}
}
}
#[test]
fn unsafe_wallet_subdirectories_are_rejected() {
for (name, value) in [("absolute", "/escape"), ("parent", "../escape"), ("nested-parent", "tests/../escape"), ("current", "./tests")] {
let fixture = fixture_roots(name);
let prepared = prepare_wallet_fixture(&fixture, value);
assert!(prepared.is_ok(), "wallet fixture should be writable: {prepared:?}");
if prepared.is_ok() {
let engine = fixture_engine(&fixture);
let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_WALLET);
assert!(engine.is_ok(), "fixture engine should be constructible: {engine:?}");
if let (std::result::Result::Ok(engine), std::result::Result::Ok(file_id)) = (engine, file_id) {
let result = engine.load_validated_document(&file_id);
assert!(result.is_err(), "unsafe wallets_subdirectory must be rejected: {name} {result:?}");
if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID);
}
}
}
cleanup_fixture(&fixture);
}
}
#[test]
fn composite_selected_wallet_profile_preserves_composite_provenance() {
let engine = workspace_engine();
assert!(engine.is_ok(), "workspace engine should be constructible: {engine:?}");
if let std::result::Result::Ok(engine) = engine {
let composite_file_id = crate::ConfigFileId::new(crate::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK);
assert!(composite_file_id.is_ok(), "Wallet Desk composite file_id should be valid: {composite_file_id:?}");
if let std::result::Result::Ok(composite_file_id) = composite_file_id {
let composite = engine.load_resolved_composite(&composite_file_id, std::option::Option::None);
assert!(composite.is_ok(), "Wallet Desk composite should resolve: {composite:?}");
if let std::result::Result::Ok(composite) = composite {
let component = composite.component("wallet");
assert!(component.is_some(), "Wallet Desk composite should expose wallet component");
if let std::option::Option::Some(component) = component {
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
let resolved = engine.resolve_wallet_config_profile(component.resolved(), &environment);
assert!(resolved.is_ok(), "composite Wallet profile should map through the runtime adapter: {resolved:?}");
if let std::result::Result::Ok(resolved) = resolved {
assert_eq!(resolved.profile_id(), "default");
assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::Composite);
}
}
}
}
}
}
#[test]
fn secret_wallet_directory_placeholder_is_rejected_by_effective_adapter() {
let fixture = fixture_roots("secret-root");
let prepared = prepare_wallet_root_fixture(&fixture, "${KSP_SECRET_WALLET_DIRECTORY}", std::option::Option::None);
assert!(prepared.is_ok(), "wallet fixture should be writable: {prepared:?}");
if prepared.is_ok() {
let engine = fixture_engine(&fixture);
assert!(engine.is_ok(), "fixture engine should be constructible: {engine:?}");
if let std::result::Result::Ok(engine) = engine {
let mut process = std::collections::BTreeMap::<String, String>::new();
process.insert("KSP_SECRET_WALLET_DIRECTORY".to_owned(), "hidden-wallets".to_owned());
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
let result = engine.load_resolved_wallet_config(std::option::Option::Some("default"), &environment);
assert!(result.is_err(), "Wallet path Config must reject Secret environment sensitivity: {result:?}");
if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID);
}
}
}
cleanup_fixture(&fixture);
}
fn workspace_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
let root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
let bootstrap = crate::ConfigBootstrapOptions::from_paths(root.join("config"), root.join("config/schemas"));
let bootstrap = match bootstrap {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let registry = crate::ConfigFileRegistry::defaults();
return match registry {
std::result::Result::Ok(value) => std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, value)),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
fn fixture_engine(fixture: &FixtureRoots) -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
let bootstrap = crate::ConfigBootstrapOptions::from_paths(fixture.config.as_path(), fixture.schemas.as_path());
let bootstrap = match bootstrap {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let registry = crate::ConfigFileRegistry::defaults();
return match registry {
std::result::Result::Ok(value) => std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, value)),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
fn prepare_wallet_fixture(fixture: &FixtureRoots, subdirectory: &str) -> std::io::Result<()> {
return prepare_wallet_root_fixture(fixture, "wallets", std::option::Option::Some(subdirectory));
}
fn prepare_wallet_root_fixture(fixture: &FixtureRoots, root: &str, subdirectory: std::option::Option<&str>) -> std::io::Result<()> {
cleanup_fixture(fixture);
let config = std::fs::create_dir_all(fixture.config.as_path());
if let std::result::Result::Err(error) = config {
return std::result::Result::Err(error);
}
let schemas = std::fs::create_dir_all(fixture.schemas.as_path());
if let std::result::Result::Err(error) = schemas {
return std::result::Result::Err(error);
}
let workspace = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
let schema = std::fs::copy(workspace.join("config/schemas/std.wallet.schema.json"), fixture.schemas.join(crate::DEFAULT_STD_WALLET_SCHEMA_FILENAME));
if let std::result::Result::Err(error) = schema {
return std::result::Result::Err(error);
}
let profile = match subdirectory {
std::option::Option::Some(value) => format!(r#"{{"profile_id":"default","wallets_subdirectory":"{value}"}}"#),
std::option::Option::None => "{\"profile_id\":\"default\"}".to_owned(),
};
let source = format!(r#"{{"format_version":1,"wallets_directory":"{root}","default_profile":"default","profiles":[{profile}]}}"#);
return std::fs::write(fixture.config.join(crate::DEFAULT_STD_WALLET_FILENAME), source);
}
fn fixture_roots(name: &str) -> FixtureRoots {
let root = std::env::temp_dir().join(format!("ksp-config-wallet-{name}-{}", std::process::id()));
return FixtureRoots { config: root.join("config"), root: root.clone(), schemas: root.join("schemas") };
}
fn cleanup_fixture(fixture: &FixtureRoots) {
let removal = std::fs::remove_dir_all(fixture.root.as_path());
if let std::result::Result::Err(error) = removal
&& error.kind() != std::io::ErrorKind::NotFound
{
eprintln!("unable to cleanup Config Wallet fixture {}: {error}", fixture.root.display());
}
}
struct FixtureRoots {
config: std::path::PathBuf,
root: std::path::PathBuf,
schemas: std::path::PathBuf,
}

247
deltas/0.2.6/pre.003.md Normal file
View File

@@ -0,0 +1,247 @@
<!-- file: deltas/0.2.6/pre.003.md -->
<!-- version: 1 -->
# Delta `0.2.6-pre.003` — `std.wallet`, composite Wallet Desk et répertoires
## Base requise
```text
0.2.6-pre.002 appliquée
workspace.package.version = 0.2.6-pre.2
```
Les preuves opérateur de `pre.002` sont vertes : installation npm initiale, `cargo fmt --all`, audit structurel Python, `cargo check --workspace`, `cargo clippy --workspace --all-targets`, `cargo test -p ksp-app-wallet-desk` et `cargo tauri dev -c crates/ksp-app-wallet-desk/tauri.conf.json`. Le shell Tauri/Vite a démarré sur le port réservé `1432`.
## Type de livraison
```text
ksp-general-0.2.6-pre.003.zip
```
Cette tranche modifie Config, le runtime Wallet Desk et les fichiers de configuration exécutables. Elle incrémente donc `workspace.package.version` vers `0.2.6-pre.3`.
## Objet
Matérialiser la composition Config nécessaire à Wallet Desk sans ouvrir encore un fichier `.kspwallet` ni implémenter l'inventory de `pre.004`.
Le delta fournit :
```text
cfg.std.wallet
schema.std.wallet
cfg.composite.ksp-app-wallet-desk
wallets_directory global
wallets_subdirectory optionnel par profil
profils Wallet default / temporary / tests
ResolvedWalletConfig
mapping d'un profil déjà résolu par composite vers Logging/Wallet
préservation ConfigProfileSelectionSource::Composite
bootstrap Wallet Desk logging + transport + wallet component validation
création automatique du répertoire global/effectif
rejet de root non-directory et de symlinks dans le sous-répertoire de profil
logs debug sur directory ready/created
logs error sur préparation impossible
statut runtime/UI Config + chemins Wallet sûrs
```
## `std.wallet`
Le document standard est enregistré sous :
```text
file_id = cfg.std.wallet
schema_file_id = schema.std.wallet
filename = std.wallet.json
schema filename = std.wallet.schema.json
```
Shape runtime :
```json
{
"format_version": 1,
"wallets_directory": "${KSP_WALLETS_DIRECTORY:-wallets}",
"default_profile": "default",
"profiles": [
{"profile_id": "default"},
{"profile_id": "temporary", "wallets_subdirectory": "temporary"},
{"profile_id": "tests", "wallets_subdirectory": "tests"}
]
}
```
`wallets_directory` est global. `wallets_subdirectory` reste relatif, optionnel et peut contenir plusieurs composants normaux. Chemin absolu, `.` ou `..` sont rejetés. Les valeurs de sensibilité `Secret` sont refusées par l'adapter Wallet : un password ne peut donc pas être injecté comme path Config.
`ksp-config-lib` résout et valide les paths mais ne crée aucun répertoire et n'énumère aucun wallet.
## Composite Wallet Desk
Le premier composite runtime concret est :
```text
file_id = cfg.composite.ksp-app-wallet-desk
filename = composite.ksp-app-wallet-desk.json
schema = schema.composite
```
Le profil autonome `devnet` sélectionne :
```text
logging -> cfg.std.logging / local_dev
transport -> cfg.std.transport / devnet_public
wallet -> cfg.std.wallet / default
```
Les profils composites `temporary` et `tests` réutilisent Logging/Transport mais sélectionnent respectivement les profils Wallet `temporary` et `tests`.
Wallet Desk exige les trois `component_id` et vérifie leurs `file_id` attendus avant de poursuivre son bootstrap. Le pool Transport n'est pas encore construit dans cette tranche ; cette composition est validée maintenant pour ne pas inventer une sélection parallèle plus tard.
## Adapters Config
`ResolvedWalletConfig` expose :
```text
file_id
source_path
profile_id
selection_source
effective Config sûr/réel selon contrat Config
wallets_directory
wallets_subdirectory
effective_wallets_directory
```
Deux entrées complètent les adapters pour un profil déjà sélectionné par composite :
```text
ConfigDocumentEngine::resolve_logging_config_profile
ConfigDocumentEngine::resolve_wallet_config_profile
```
Elles évitent de recharger le même profil comme sélection `Explicit` et conservent `ConfigProfileSelectionSource::Composite`.
Cette extension ne crée aucune dépendance Config -> `ksp-wallet-lib`.
## Répertoires Wallet
Après initialisation du Logging, Wallet Desk résout le composant Wallet et prépare la filesystem surface applicative :
```text
wallets_directory
-> création si absent
-> optional wallets_subdirectory composant par composant
-> effective_wallets_directory prêt
```
Politique de cette tranche :
```text
root déjà directory -> accepté
root absent -> create_dir_all
root existant non-directory -> wallet_desk.wallet_directory_invalid
profile component absent -> create_dir
profile component directory -> accepté
profile component symlink -> wallet_desk.wallet_directory_invalid
profile component fichier -> wallet_desk.wallet_directory_invalid
inspection/création I/O KO -> wallet_desk.wallet_directory_prepare_failed
```
Un succès/prêt est journalisé en `debug`. Une impossibilité de préparation est journalisée en `error` puis surfacée au bootstrap. Les paths sont non secrets par contrat `std.wallet`.
L'inventory `.kspwallet`, la canonicalisation des fichiers candidats et la policy complète de sélection de fichiers restent `pre.004`.
## Environnement
`/.env.example` documente désormais :
```text
KSP_WALLETS_DIRECTORY=wallets
# KSP_SECRET_WALLET_PASS_01=
```
La seconde ligne documente uniquement le pattern prévu pour la future tranche d'unlock. Aucun provider de password n'est implémenté dans `pre.003` et aucune valeur `KSP_SECRET_WALLET_PASS_*` n'est lue par Wallet Desk à ce stade.
## UI/runtime status
Le DTO `RuntimeStatusDto` et le Dashboard affichent maintenant :
```text
version / phase
nombre de ressources Config enregistrées
profil composite
profil Logging
profil Wallet
fallback Logging
wallets_directory
wallets_subdirectory
répertoire Wallet effectif
création du répertoire effectif au démarrage oui/non
```
Aucune Pubkey, aucun alias interne Wallet, aucune note et aucun secret ne sont ajoutés à cette projection.
## Tests ajoutés/étendus
Config :
```text
registre à 8 descripteurs
cfg.std.wallet + schema + composite enregistrés
schema std.wallet
root global sans subdirectory
profil tests -> subdirectory relatif
precedence process > .env pour KSP_WALLETS_DIRECTORY
rejet absolute / parent / current subdirectories
rejet d'un path Wallet de sensibilité Secret
profil Wallet sélectionné par composite conserve provenance Composite
public API des nouveaux adapters/constants
ownership des filenames Config-managed
```
Wallet Desk :
```text
composite devnet -> logging/local_dev + transport/devnet_public + wallet/default
création idempotente root + nested profile directory
rejet root non-directory
rejet symlink de profile path sur Unix
statut frontend expose les projections Config prévues
```
## Dépendances
Aucune dépendance Cargo ou npm n'est ajoutée/modifiée dans cette tranche. `package.json` et `tauri.conf.json` changent uniquement leur version applicative vers `0.2.6-pre.3`.
Il n'y a donc **aucune commande npm directe à exécuter pour `pre.003`**.
## Version technique
```text
workspace.package.version = 0.2.6-pre.3
commit = v0.2.6-pre.003
```
Aucun tag stable.
## Validation opérateur requise
Après application :
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-config-lib
cargo test -p ksp-app-wallet-desk
cargo test --workspace
KSP_WALLETS_DIRECTORY=var/wallet-desk-pre003 cargo tauri dev -c crates/ksp-app-wallet-desk/tauri.conf.json
```
Le parcours Tauri doit vérifier que le Dashboard affiche le profil composite `devnet`, le profil Wallet `default`, la racine override `var/wallet-desk-pre003` résolue et le répertoire effectif prêt/créé, avec événement backend `debug` correspondant. Le répertoire `var/` est déjà ignoré par le repository.
`cargo tauri build -c crates/ksp-app-wallet-desk/tauri.conf.json` **n'est pas exécuté en `pre.003`**. Il reste la toute dernière opération de validation de `0.2.6`.
## Suite
`pre.004` branche l'inventory réel : enumeration non récursive `.kspwallet`, policy symlink/path, `inspect_locked_wallet_file_v1`, DTOs locked, DataTables, sélection de ligne et refresh, sans révéler Pubkey/alias/notes.

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
<!-- file: docs/architecture/005-DEPENDENCY_GRAPH.md --> <!-- file: docs/architecture/005-DEPENDENCY_GRAPH.md -->
<!-- version: 11 --> <!-- version: 12 -->
# Graphe de dépendances KSP # Graphe de dépendances KSP
@@ -388,7 +388,7 @@ ksp-app-wallet-desk
-> ksp-logging-lib -> ksp-logging-lib
``` ```
L'app compose ; elle ne déplace pas Config/Wallet/Transport dans Tauri. Les handles `WalletView`/`WalletOwner` restent côté Rust, et le frontend ne reçoit que des DTOs sûrs. Config peut fournir la racine Wallet et le profil Transport ; les passwords et le secret Solana ne deviennent jamais des valeurs Config. L'app compose ; elle ne déplace pas Config/Wallet/Transport dans Tauri. Les handles `WalletView`/`WalletOwner` restent côté Rust, et le frontend ne reçoit que des DTOs sûrs. Config fournit la racine/profil Wallet et le profil Transport. Les passwords Wallet ne deviennent jamais des champs des documents JSON Config ; ils peuvent être saisis éphémèrement frontend -> Rust ou provenir plus tard de secrets process/`.env` `KSP_SECRET_WALLET_PASS_*` possédés exclusivement par Config. Le secret Solana ne devient jamais une valeur Config ni un DTO frontend.
## Price Desk ## Price Desk

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/000-README.md --> <!-- file: docs/plans/000-README.md -->
<!-- version: 46 --> <!-- version: 48 -->
# Plans KSP # Plans KSP
@@ -21,7 +21,7 @@ Un plan décrit le périmètre, les décisions déjà acquises, les questions ou
- [`010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md`](010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md) — plan historique clôturé de la release stable `0.2.3 — HTTP Transactions`, ouvert par `pre.001`, exécuté jusqu'à `pre.009` puis publié par `rel.001`; il couvre les 11 méthodes, la classification `8 Read / 2 WriteSubmission / 1 Simulation`, `KSP-TRANSPORT-007`, le no-resend et la préparation de `0.2.4`. - [`010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md`](010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md) — plan historique clôturé de la release stable `0.2.3 — HTTP Transactions`, ouvert par `pre.001`, exécuté jusqu'à `pre.009` puis publié par `rel.001`; il couvre les 11 méthodes, la classification `8 Read / 2 WriteSubmission / 1 Simulation`, `KSP-TRANSPORT-007`, le no-resend et la préparation de `0.2.4`.
- [`011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md`](011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md) — plan historique clôturé de la release stable `0.2.4`, ouvert par `pre.001`, exécuté jusquà `pre.009`, complété par le fix documentaire Wallet `pre.009-fix.001` puis publié par `rel.001`; il couvre les 10 Blocks + 5 Economics et la compliance finale `52/52 + 14/14` sous `KSP-TRANSPORT-007`. - [`011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md`](011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md) — plan historique clôturé de la release stable `0.2.4`, ouvert par `pre.001`, exécuté jusquà `pre.009`, complété par le fix documentaire Wallet `pre.009-fix.001` puis publié par `rel.001`; il couvre les 10 Blocks + 5 Economics et la compliance finale `52/52 + 14/14` sous `KSP-TRANSPORT-007`.
- [`012-V0_2_5_WALLET_FOUNDATION_PLAN.md`](012-V0_2_5_WALLET_FOUNDATION_PLAN.md) — plan historique clôturé de la release stable `0.2.5 — Wallet foundation`, ouvert par `pre.001`, livré jusquà `pre.010`, renforcé par `pre.010-fix.001``fix.003` pour Dalek 3 et la normalisation Rust/audit structurel, puis publié par `rel.001`; il couvre `.kspwallet` V1, VIEW/OWNER, crypto, persistence, administration, transfer et compliance. - [`012-V0_2_5_WALLET_FOUNDATION_PLAN.md`](012-V0_2_5_WALLET_FOUNDATION_PLAN.md) — plan historique clôturé de la release stable `0.2.5 — Wallet foundation`, ouvert par `pre.001`, livré jusquà `pre.010`, renforcé par `pre.010-fix.001``fix.003` pour Dalek 3 et la normalisation Rust/audit structurel, puis publié par `rel.001`; il couvre `.kspwallet` V1, VIEW/OWNER, crypto, persistence, administration, transfer et compliance.
- [`013-V0_2_6_WALLET_DESK_PLAN.md`](013-V0_2_6_WALLET_DESK_PLAN.md) — plan actif de `0.2.6 — Wallet Desk`, ouvert par `pre.001`; il fixe la composition Config/Wallet/Transport/Logging, `std.wallet`, inventory/path safety, lifecycle VIEW/OWNER, screen/command/DTO maps, threat model Tauri, balance HTTP, administration retenue et forecast regranularisé jusquà `pre.013`. - [`013-V0_2_6_WALLET_DESK_PLAN.md`](013-V0_2_6_WALLET_DESK_PLAN.md) — plan actif de `0.2.6 — Wallet Desk`, ouvert par `pre.001`; `pre.002` matérialise le shell Tauri et `pre.003` la composition `std.wallet`/composite ainsi que la préparation des répertoires. Le plan fixe aussi secrets `KSP_SECRET_WALLET_PASS_*`, inventory DataTables/path safety, lifecycle VIEW/OWNER, screen/command/DTO maps, balance HTTP, administration retenue et forecast souple détaillé jusquà `pre.014`, incluant la documentation finale et le prompt `0.2.7`.
Le `pre.001` de chaque release fonctionnelle peut introduire son propre plan détaillé lorsque la release s'ouvre. Le `pre.001` de chaque release fonctionnelle peut introduire son propre plan détaillé lorsque la release s'ouvre.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md --> <!-- file: docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md -->
<!-- version: 56 --> <!-- version: 58 -->
# Séquence des releases fonctionnelles KSP # Séquence des releases fonctionnelles KSP
@@ -424,11 +424,13 @@ La release fournit `docs/formats/KSPWALLET_V1.md` comme spécification séparée
Mission : valider Config composite + `.kspwallet` + transport HTTP dans une application Tauri mince. Mission : valider Config composite + `.kspwallet` + transport HTTP dans une application Tauri mince.
Le solde d'un wallet constitue un premier cas de validation réseau obligatoire. L'application garde les handles VIEW/OWNER côté Rust, ne persiste aucun password dans Config/frontend et délègue toute crypto/signature/administration à `ksp-wallet-lib`. Le solde d'un wallet constitue un premier cas de validation réseau obligatoire. L'application garde les handles VIEW/OWNER côté Rust, ne persiste aucun password dans les documents JSON Config ni dans le frontend et délègue toute crypto/signature/administration à `ksp-wallet-lib`. Des secrets opérateur `KSP_SECRET_WALLET_PASS_*` peuvent être fournis par process env ou `.env`, exclusivement via les APIs Config et sans traverser le frontend.
Le prompt [`../../prompts/011-V0_2_6_START_PROMPT.md`](../../prompts/011-V0_2_6_START_PROMPT.md), préparé par `0.2.5-pre.010`, impose un `pre.001` d'audit/sizing couvrant Config `std.wallet`/composite, inventory/path safety, lifecycle VIEW/OWNER, balance `getBalance`, bridge Tauri/TS-RS, sécurité password/export, logging et validation finale Tauri. Le prompt [`../../prompts/011-V0_2_6_START_PROMPT.md`](../../prompts/011-V0_2_6_START_PROMPT.md), préparé par `0.2.5-pre.010`, impose un `pre.001` d'audit/sizing couvrant Config `std.wallet`/composite, inventory/path safety, lifecycle VIEW/OWNER, balance `getBalance`, bridge Tauri/TS-RS, sécurité password/export, logging et validation finale Tauri.
`0.2.6-pre.001` confirme que le registre Config doit être étendu pour `cfg.std.wallet`/`schema.std.wallet` et `cfg.composite.ksp-app-wallet-desk`; `wallets_directory` reste global avec fallback `${KSP_WALLETS_DIRECTORY:-wallets}` tandis qu'un profil `default` minimal préserve le contrat de résolution composite. Le gate retient une inventory non récursive `.kspwallet`, sans symlink, root-scoped, les handles VIEW/OWNER exclusivement côté Rust, une CSP Tauri explicite, `getBalance` via la Pubkey du handle autorisé et les flows OWNER metadata/rotations/revocation/export fichier déjà possédés par Wallet. Le scope tient dans `0.2.6` mais le forecast initial est regranularisé jusqu'à `pre.013`; le plan actif est `docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md`. `0.2.6-pre.001` confirme que le registre Config doit être étendu pour `cfg.std.wallet`/`schema.std.wallet` et `cfg.composite.ksp-app-wallet-desk`. `wallets_directory` reste global avec fallback `${KSP_WALLETS_DIRECTORY:-wallets}` et chaque profil peut définir un `wallets_subdirectory` relatif pour isoler tests, temporaires ou futurs scénarios. Wallet Desk prépare/crée le répertoire effectif après résolution Config et journalise le succès en debug ou l'échec en error. Le gabarit Config Desk est réutilisé avec Bootstrap, Font Awesome, DataTables/Select, SimpleBar et `resize-observer-polyfill`. L'inventory reste non récursive `.kspwallet`, sans symlink d'entrée et root-scoped; les handles VIEW/OWNER restent exclusivement côté Rust. L'ouverture accepte un password éphémère saisi ou une tentative explicite de secrets Config `KSP_SECRET_WALLET_PASS_*`, dont ni nom ni valeur ne traversent le frontend. `getBalance` utilise la Pubkey du handle autorisé et les flows OWNER metadata/rotations/revocation/export fichier délèguent à Wallet. La CSP reste `null` par défaut comme dans Config Desk et n'est réauditée qu'en présence d'un besoin WebView concret. Le forecast souple détaillé s'étend jusqu'à `pre.014`, dernière tranche prévue pour README/USAGE/docs/validation, prompt `0.2.7` et build Tauri final; le plan actif est `docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md`.
`0.2.6-pre.002` matérialise le shell Tauri spécialisé. `0.2.6-pre.003` matérialise ensuite la première composition Config propre à Wallet Desk : `cfg.std.wallet`/`schema.std.wallet`, le composite `cfg.composite.ksp-app-wallet-desk`, `ResolvedWalletConfig`, la racine `${KSP_WALLETS_DIRECTORY:-wallets}`, les sous-répertoires de profils `temporary`/`tests`, la préservation de provenance `Composite` dans les adapters Logging/Wallet et la création sûre des répertoires par lapplication. Aucun inventory ou fichier `.kspwallet` nest encore ouvert dans cette tranche.
## `0.2.7` — WebSocket Solana standard ## `0.2.7` — WebSocket Solana standard

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md --> <!-- file: docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md -->
<!-- version: 5 --> <!-- version: 6 -->
# Plan `0.2.6` — Wallet Desk # Plan `0.2.6` — Wallet Desk
@@ -23,6 +23,8 @@ Les validations opérateur du `2026-08-20` ont ensuite confirmé la base `pre.00
`0.2.6-pre.002` matérialise ensuite la première tranche technique : la crate `ksp-app-wallet-desk` rejoint le workspace avec le shell splash/main du gabarit Config Desk, le bootstrap Config + Logging commun, les ports `1432/1433`, Bootstrap, Font Awesome, DataTables/Select, SimpleBar, `resize-observer-polyfill`, TS-RS et le bridge frontend -> Rust -> `ksp-logging-lib`. Cette tranche ne branche encore ni `std.wallet`, ni inventory filesystem réel, ni `ksp-wallet-lib`, ni Transport : ces responsabilités restent dans les prereleases déjà dimensionnées. `0.2.6-pre.002` matérialise ensuite la première tranche technique : la crate `ksp-app-wallet-desk` rejoint le workspace avec le shell splash/main du gabarit Config Desk, le bootstrap Config + Logging commun, les ports `1432/1433`, Bootstrap, Font Awesome, DataTables/Select, SimpleBar, `resize-observer-polyfill`, TS-RS et le bridge frontend -> Rust -> `ksp-logging-lib`. Cette tranche ne branche encore ni `std.wallet`, ni inventory filesystem réel, ni `ksp-wallet-lib`, ni Transport : ces responsabilités restent dans les prereleases déjà dimensionnées.
`0.2.6-pre.003` matérialise la composition Config prévue : `cfg.std.wallet`/`schema.std.wallet`, le composite `cfg.composite.ksp-app-wallet-desk`, les profils Wallet `default`/`temporary`/`tests`, `ResolvedWalletConfig` et les adapters qui peuvent consommer un profil déjà sélectionné par composite sans perdre la provenance `Composite`. Wallet Desk valide les composants `logging`/`transport`/`wallet`, crée la racine et le sous-répertoire effectif absents avec logs `debug`, refuse les objets filesystem invalides/symlinks de sous-répertoire avec logs `error`, et expose uniquement les chemins/profils non secrets dans le statut runtime. Aucun inventory `.kspwallet` nest encore effectué.
## 2. Sources relues et hiérarchie appliquée ## 2. Sources relues et hiérarchie appliquée
L'audit suit l'ordre d'autorité demandé : L'audit suit l'ordre d'autorité demandé :
@@ -1244,22 +1246,25 @@ Le shell peut afficher des données mockées/runtime minimales, mais doit déjà
### `pre.003` — `std.wallet`, composite et préparation des répertoires ### `pre.003` — `std.wallet`, composite et préparation des répertoires
Objectifs : Livrables de la tranche :
```text ```text
cfg.std.wallet / schema.std.wallet cfg.std.wallet / schema.std.wallet enregistrés dans Config
wallets_directory global wallets_directory global = ${KSP_WALLETS_DIRECTORY:-wallets}
wallets_subdirectory par profil wallets_subdirectory optionnel, relatif et multi-composants normaux
ResolvedWalletConfig profils default / temporary / tests
cfg.composite.ksp-app-wallet-desk ResolvedWalletConfig avec root/subdirectory/effective path/provenance
profils default/tests/temporary ou équivalents retenus resolve_logging_config_profile / resolve_wallet_config_profile pour profils composites
.env.example pour KSP_WALLETS_DIRECTORY et pattern KSP_SECRET_WALLET_PASS_* cfg.composite.ksp-app-wallet-desk = logging + transport + wallet
bootstrap Config Wallet Desk .env.example documente KSP_WALLETS_DIRECTORY et le pattern futur KSP_SECRET_WALLET_PASS_*
ensure/create global + effective directories bootstrap Wallet Desk valide les trois component_id/file_id requis
debug success / error failure création automatique du root et du sous-répertoire effectif absents
rejet d'un root non-directory et d'un composant de sous-répertoire symlink/non-directory
debug sur répertoires prêts/créés ; error sur préparation impossible
status DTO/UI : profil composite, profils Logging/Wallet, root/subdirectory/effective directory
``` ```
Tests : schema, composition, containment, création de répertoires. Tests de tranche : registre/schema, composition et provenance, precedence environnement, containment syntaxique, création idempotente des répertoires, rejet non-directory/symlink et contrats desktop. Linventory `.kspwallet` reste strictement `pre.004`.
### `pre.004` — inventory DataTables et inspection locked ### `pre.004` — inventory DataTables et inspection locked