diff --git a/Cargo.toml b/Cargo.toml index 28e22d8..c78fa8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 468 +# version: 469 [workspace] resolver = "3" members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"] [workspace.package] -version = "0.3.8-pre.10.fix.2" +version = "0.3.8-pre.11" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-app-store-desk/tests/release_completeness.rs b/crates/ksp-app-store-desk/tests/release_completeness.rs new file mode 100644 index 0000000..d9c7045 --- /dev/null +++ b/crates/ksp-app-store-desk/tests/release_completeness.rs @@ -0,0 +1,369 @@ +// file: crates/ksp-app-store-desk/tests/release_completeness.rs +// version: 1 + +//! Release-completeness firewall for the Store Desk V1 RAW surface. + +fn parse_json(source: &str) -> serde_json::Result { + return serde_json::from_str::(source); +} + +fn toml_table_keys<'a>(source: &'a str, table: &str) -> std::vec::Vec<&'a str> { + let header = format!("[{table}]"); + let mut in_table = false; + let mut keys = std::vec::Vec::<&str>::new(); + for line in source.lines() { + let trimmed = line.trim(); + if trimmed == header { + in_table = true; + continue; + } + if in_table && trimmed.starts_with('[') { + break; + } + if !in_table || trimmed.is_empty() || trimmed.starts_with('#') { + continue; + } + if let std::option::Option::Some((key, _)) = trimmed.split_once('=') { + let key = key.trim(); + let normalized = match key.strip_suffix(".workspace") { + std::option::Option::Some(value) => value, + std::option::Option::None => key, + }; + keys.push(normalized); + } + } + keys.sort_unstable(); + return keys; +} + +fn json_object_keys(value: &serde_json::Value) -> std::vec::Vec<&str> { + let mut keys = match value.as_object() { + std::option::Option::Some(object) => object.keys().map(std::string::String::as_str).collect::>(), + std::option::Option::None => std::vec::Vec::<&str>::new(), + }; + keys.sort_unstable(); + return keys; +} + +fn source_segment<'a>(source: &'a str, start: &str, end: &str) -> &'a str { + let after_start = match source.split_once(start) { + std::option::Option::Some((_, tail)) => tail, + std::option::Option::None => return "", + }; + return match after_start.split_once(end) { + std::option::Option::Some((head, _)) => head, + std::option::Option::None => after_start, + }; +} + +fn handler_commands(source: &str) -> std::vec::Vec<&str> { + let block = source_segment(source, "tauri::generate_handler![", "]);\n}"); + let mut commands = block.lines().map(str::trim).filter(|line| !line.is_empty()).map(|line| line.trim_end_matches(',')).collect::>(); + commands.sort_unstable(); + return commands; +} + +fn crate_modules(source: &str) -> std::vec::Vec<&str> { + let mut modules = source + .lines() + .map(str::trim) + .filter(|line| line.starts_with("mod ") && line.ends_with(';')) + .map(|line| line.trim_start_matches("mod ").trim_end_matches(';')) + .collect::>(); + modules.sort_unstable(); + return modules; +} + +fn visible_struct_names(source: &str) -> std::vec::Vec<&str> { + let mut names = source + .lines() + .map(str::trim) + .filter(|line| line.starts_with("pub(crate) struct ")) + .filter_map(|line| line.strip_prefix("pub(crate) struct ")) + .filter_map(|line| line.split_whitespace().next()) + .map(|name| name.trim_end_matches('{')) + .collect::>(); + names.sort_unstable(); + return names; +} + +fn const_line<'a>(source: &'a str, name: &str) -> &'a str { + let prefix = format!("const {name}:"); + for line in source.lines() { + if line.starts_with(prefix.as_str()) { + return line; + } + } + return ""; +} + +#[test] +fn pre_011_rust_and_frontend_dependency_inventories_are_exact() { + let manifest = include_str!("../Cargo.toml"); + assert_eq!(toml_table_keys(manifest, "build-dependencies"), ["tauri-build"]); + assert_eq!( + toml_table_keys(manifest, "dependencies"), + [ + "chrono", + "fs2", + "ksp-config-lib", + "ksp-core-lib", + "ksp-logging-lib", + "ksp-store-lib", + "serde", + "tauri", + "tauri-plugin-tracing", + "tokio", + "ts-rs", + ] + ); + assert_eq!(toml_table_keys(manifest, "dev-dependencies"), ["serde_json"]); + let package = parse_json(include_str!("../package.json")); + assert!(package.is_ok(), "Store Desk package.json must stay valid JSON"); + if let std::result::Result::Ok(package) = package { + assert_eq!( + json_object_keys(&package["dependencies"]), + [ + "@fltsci/tauri-plugin-tracing", + "@fortawesome/fontawesome-free", + "@tauri-apps/api", + "bootstrap", + "datatables.net-bs5", + "resize-observer-polyfill", + "simplebar", + ] + ); + assert_eq!( + json_object_keys(&package["devDependencies"]), + ["@tauri-apps/cli", "@types/bootstrap", "@types/node", "sass-embedded", "typescript", "vite"] + ); + } +} + +#[test] +fn pre_011_module_command_and_tauri_capability_inventories_are_exact() { + let lib = include_str!("../src/lib.rs"); + assert_eq!( + crate_modules(lib), + [ + "app_state", + "bootstrap", + "constants", + "dto_account", + "dto_common", + "dto_observation", + "dto_transaction", + "errors", + "frontend_logging", + "logging_runtime", + "splash", + "store_runtime", + "tauri", + "tw_main", + "tw_splash", + ] + ); + let tauri = include_str!("../src/tauri.rs"); + assert_eq!( + handler_commands(tauri), + [ + "emit_frontend_log", + "get_shell_status", + "splash_frontend_ready", + "store_get_account_detail", + "store_get_transaction_detail", + "store_query_account_observations", + "store_query_accounts", + "store_query_transaction_observations", + "store_query_transactions", + "store_runtime_status", + ] + ); + let capability = parse_json(include_str!("../capabilities/default.json")); + assert!(capability.is_ok(), "Store Desk capability JSON must stay valid"); + if let std::result::Result::Ok(capability) = capability { + assert_eq!(capability["identifier"], "default"); + assert_eq!(capability["windows"], serde_json::json!(["splash", "main"])); + assert_eq!(capability["permissions"], serde_json::json!(["core:default", "tracing:default"])); + } +} + +#[test] +fn pre_011_config_composition_is_exact_network_scoped_and_secret_free() { + let source = include_str!("../../../config/composite.ksp-app-store-desk.json"); + let composite = parse_json(source); + assert!(composite.is_ok(), "Store Desk composite must stay valid JSON"); + if let std::result::Result::Ok(composite) = composite { + assert_eq!(composite["format_version"], 1); + assert_eq!(composite["default_profile"], "mainnet"); + let profiles = composite["profiles"].as_array(); + assert!(profiles.is_some()); + if let std::option::Option::Some(profiles) = profiles { + assert_eq!(profiles.len(), 3); + let profile_ids = profiles + .iter() + .map(|profile| match profile["profile_id"].as_str() { + std::option::Option::Some(value) => value, + std::option::Option::None => "", + }) + .collect::>(); + assert_eq!(profile_ids, ["devnet", "mainnet", "testnet"]); + for profile in profiles { + let profile_id = match profile["profile_id"].as_str() { + std::option::Option::Some(value) => value, + std::option::Option::None => "", + }; + let documents = profile["documents"].as_array(); + assert!(documents.is_some()); + if let std::option::Option::Some(documents) = documents { + assert_eq!(documents.len(), 2); + assert_eq!(documents[0]["component_id"], "logging"); + assert_eq!(documents[0]["file_id"], "cfg.std.logging"); + assert_eq!(documents[0]["profile_id"], "supertrace"); + assert_eq!(documents[1]["component_id"], "store"); + assert_eq!(documents[1]["file_id"], "cfg.std.store"); + assert_eq!(documents[1]["profile_id"], profile_id); + } + } + } + } + for forbidden in ["postgres://", "postgresql://", "password", "credential", "connection_uri", "connectionUri", "KSP_STORE_POSTGRES"] { + assert!(!source.contains(forbidden), "Store Desk composite leaked physical/secret material: {forbidden}"); + } +} + +#[test] +fn pre_011_frontend_security_scan_keeps_network_storage_native_dialogs_and_physical_store_out() { + let frontend = [ + include_str!("../frontend/ts/frontend_log.ts"), + include_str!("../frontend/ts/invoke.ts"), + include_str!("../frontend/ts/main.ts"), + include_str!("../frontend/ts/splash.ts"), + include_str!("../frontend/main.html"), + include_str!("../frontend/splash.html"), + ] + .join("\n"); + for forbidden in [ + "fetch(", + "XMLHttpRequest", + "WebSocket(", + "EventSource(", + "localStorage", + "sessionStorage", + "indexedDB", + "document.cookie", + "window.open(", + "alert(", + "confirm(", + "prompt(", + "postgres://", + "postgresql://", + "connection_uri", + "connectionUri", + "ksp_store_postgres", + "tokio_postgres", + "deadpool_postgres", + "SELECT ", + "INSERT ", + "UPDATE ", + "DELETE ", + ] { + assert!(!frontend.contains(forbidden), "Store Desk frontend crossed the release security firewall: {forbidden}"); + } +} + +#[test] +fn pre_011_ipc_dto_inventory_is_exact_and_table_contracts_remain_raw_byte_free() { + let dto_account = include_str!("../src/dto_account.rs"); + let dto_common = include_str!("../src/dto_common.rs"); + let dto_observation = include_str!("../src/dto_observation.rs"); + let dto_transaction = include_str!("../src/dto_transaction.rs"); + let dto_sources = [dto_account, dto_common, dto_observation, dto_transaction].join("\n"); + assert_eq!( + visible_struct_names(dto_sources.as_str()), + [ + "CommandErrorDto", + "ShellStatusDto", + "StoreAccountDetailDto", + "StoreAccountDetailRequestDto", + "StoreAccountObservationQueryRequestDto", + "StoreAccountObservationQueryResponseDto", + "StoreAccountObservationRowDto", + "StoreAccountQueryRequestDto", + "StoreAccountQueryResponseDto", + "StoreAccountRowDto", + "StoreObservationProvenanceDto", + "StoreRuntimeStatusDto", + "StoreTransactionDetailDto", + "StoreTransactionDetailRequestDto", + "StoreTransactionObservationQueryRequestDto", + "StoreTransactionObservationQueryResponseDto", + "StoreTransactionObservationRowDto", + "StoreTransactionQueryRequestDto", + "StoreTransactionQueryResponseDto", + "StoreTransactionRowDto", + ] + ); + for request_marker in [ + "pub(crate) struct StoreTransactionQueryRequestDto", + "pub(crate) struct StoreTransactionDetailRequestDto", + "pub(crate) struct StoreAccountQueryRequestDto", + "pub(crate) struct StoreAccountDetailRequestDto", + "pub(crate) struct StoreTransactionObservationQueryRequestDto", + "pub(crate) struct StoreAccountObservationQueryRequestDto", + ] { + assert!(dto_sources.contains(request_marker), "missing Store Desk request DTO: {request_marker}"); + } + for forbidden in ["RawPageCursor", "tokio_postgres", "deadpool_postgres", "ksp_store_postgres", "source_payload_bytes", "raw_bytes"] { + assert!(!dto_sources.contains(forbidden), "Store Desk DTO surface leaked forbidden contract material: {forbidden}"); + } + assert!(!source_segment(dto_transaction, "pub(crate) struct StoreTransactionRowDto", "/// Bounded detail projection").contains("payload_preview")); + assert!(!source_segment(dto_account, "pub(crate) struct StoreAccountRowDto", "/// Bounded detail projection").contains("data_preview")); + assert!(!dto_observation.contains("payload_preview")); + assert!(!dto_observation.contains("data_preview")); +} + +#[test] +fn pre_011_cursor_keyset_navigation_and_random_access_inspection_coexist_without_contract_collision() { + let api = include_str!("../../ksp-store-api/src/lib.rs"); + let facade = include_str!("../../ksp-store-lib/src/lib.rs"); + let transaction = include_str!("../../ksp-store-postgres-lib/src/raw_transaction.rs"); + let account = include_str!("../../ksp-store-postgres-lib/src/raw_account.rs"); + let app_runtime = include_str!("../src/store_runtime.rs"); + for expected in ["RawPageCursor", "RawPageRequest", "RawInspectionPageRequest", "RawTransactionInspectionQuery", "RawAccountStateInspectionQuery"] { + assert!(api.contains(expected), "Store API lost pagination/inspection contract: {expected}"); + assert!(facade.contains(expected), "Store facade lost pagination/inspection re-export: {expected}"); + } + for cursor_statement in ["LIST_TRANSACTIONS_ASC_SQL", "LIST_TRANSACTIONS_DESC_SQL"] { + let statement = const_line(transaction, cursor_statement); + assert!(!statement.is_empty(), "missing transaction cursor statement: {cursor_statement}"); + assert!(!statement.contains("OFFSET"), "transaction cursor statement became OFFSET based: {cursor_statement}"); + } + for cursor_statement in [ + "LIST_ACCOUNT_STATES_ASC_SQL", + "LIST_ACCOUNT_STATES_DESC_SQL", + "LIST_ACCOUNT_STATES_BY_PUBKEY_ASC_SQL", + "LIST_ACCOUNT_STATES_BY_PUBKEY_DESC_SQL", + ] { + let statement = const_line(account, cursor_statement); + assert!(!statement.is_empty(), "missing account cursor statement: {cursor_statement}"); + assert!(!statement.contains("OFFSET"), "account cursor statement became OFFSET based: {cursor_statement}"); + } + for inspection_statement in + ["INSPECT_TRANSACTIONS_ASC_SQL", "INSPECT_TRANSACTIONS_DESC_SQL", "INSPECT_OBSERVATIONS_ASC_SQL", "INSPECT_OBSERVATIONS_DESC_SQL"] + { + let statement = const_line(transaction, inspection_statement); + assert!(statement.contains("OFFSET"), "transaction inspection statement lost random access: {inspection_statement}"); + } + for inspection_statement in [ + "INSPECT_ACCOUNT_STATES_ASC_SQL", + "INSPECT_ACCOUNT_STATES_DESC_SQL", + "INSPECT_ACCOUNT_OBSERVATIONS_ASC_SQL", + "INSPECT_ACCOUNT_OBSERVATIONS_DESC_SQL", + ] { + let statement = const_line(account, inspection_statement); + assert!(statement.contains("OFFSET"), "account inspection statement lost random access: {inspection_statement}"); + } + assert!(app_runtime.contains("RawInspectionPageRequest::new")); + assert!(!app_runtime.contains("RawPageCursor")); +} diff --git a/deltas/0.3.8/pre.011.md b/deltas/0.3.8/pre.011.md new file mode 100644 index 0000000..11ade08 --- /dev/null +++ b/deltas/0.3.8/pre.011.md @@ -0,0 +1,48 @@ + + + +# Delta `0.3.8-pre.011` + +## Base + +- livraison directe : `0.3.8-pre.010-fix.002` ; +- workspace : `0.3.8-pre.10.fix.2` ; +- gate opérateur précédent : entièrement vert, sans warning Clippy. + +## Objet + +Fermer la sécurité, les dependency boundaries et la release completeness de Store Desk V1 RAW sans ajouter de fonctionnalité runtime. + +## Changements + +- ajout de `tests/release_completeness.rs` ; +- inventaires Cargo/npm exacts ; +- inventaires modules Rust, commands Tauri et permissions capability exacts ; +- composition Config Store Desk exacte et secret-free ; +- scan frontend no-network/no-browser-storage/no-native-dialog/no-physical-Store ; +- inventaire DTO/IPC exact et metadata-only ; +- canari explicite de coexistence cursor/keyset machine et inspection offset/limit DataTables ; +- synchronisation workspace vers `0.3.8-pre.11` ; +- plan et validation mis à jour. + +## Hors périmètre + +- aucun runtime/frontend/DTO modifié ; +- aucun Store API/façade/PostgreSQL/SQL/schema/migration modifié ; +- aucune Config ou capability Tauri modifiée ; +- aucune nouvelle dépendance. + +## Gate recommandé + +```bash +cargo fmt --all +python3 scripts/audit_rust_workspace_rules.py +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas + +cargo check --workspace +cargo clippy --workspace --all-targets + +cargo test -p ksp-app-store-desk +cargo test -p ksp-store-lib +cargo check -p ksp-store-lib --no-default-features +``` diff --git a/docs/plans/029-V0_3_8_STORE_DESK_PLAN.md b/docs/plans/029-V0_3_8_STORE_DESK_PLAN.md index ba51c93..8d9e3d8 100644 --- a/docs/plans/029-V0_3_8_STORE_DESK_PLAN.md +++ b/docs/plans/029-V0_3_8_STORE_DESK_PLAN.md @@ -1,5 +1,5 @@ - + # Plan v0.3.8 — Store Desk V1 RAW @@ -622,7 +622,9 @@ Le hardening V1 centralise la validation offset/limit côté Rust, rejette les d ### pre.011 — security / dependency / release completeness -Dependency firewall, Config composition, IPC/security scans, no-browser-storage, exact module/dependency/capability inventories, coexistence cursor + inspection. +Verrouiller la surface V1 sans ajouter de fonctionnalité métier ni modifier Store API/façade/PostgreSQL. Un nouveau firewall exécutable `release_completeness.rs` doit rendre exacts les inventaires directs Cargo/npm, les modules Rust Store Desk, les commands Tauri et les permissions desktop. La composition Config Store Desk reste exactement `logging + store` sur `devnet/mainnet/testnet`, sans URI ni secret dans le composite. + +Le frontend est rescanné contre toute ouverture réseau navigateur, stockage navigateur RAW, dialogue natif ou matérialisation PostgreSQL/SQL. Les DTO/IPC doivent rester backend-neutral et metadata-only, avec les previews RAW bornés uniquement dans les DTO de détail déjà admis. Enfin, la coexistence des deux paradigmes de pagination est verrouillée explicitement : `RawPageCursor`/`RawPageRequest` et les statements PostgreSQL `LIST_*` restent keyset et OFFSET-free pour les machines/workers, tandis que `RawInspectionPageRequest` et les statements `INSPECT_*` conservent leur random access `OFFSET` pour Store Desk/DataTables. Aucun runtime n'est ajouté par cette tranche. ### pre.012 — gate technique final diff --git a/docs/validation/025-V0_3_8_STORE_DESK.md b/docs/validation/025-V0_3_8_STORE_DESK.md index 153f986..5447722 100644 --- a/docs/validation/025-V0_3_8_STORE_DESK.md +++ b/docs/validation/025-V0_3_8_STORE_DESK.md @@ -1,5 +1,5 @@ - + # Validation v0.3.8 — Store Desk V1 RAW @@ -785,3 +785,40 @@ Le gate opérateur de `0.3.8-pre.10.fix.1` confirme que les audits KSP, `cargo c - [X] la version workspace devient `0.3.8-pre.10.fix.2` car un test Rust est modifié. Le gate Cargo du fix reste à rejouer après application du delta. + +## 38. Clôture `pre.010-fix.002` et préparation `pre.011` — release completeness + +Le gate opérateur de `0.3.8-pre.10.fix.2` est entièrement vert : + +```text +General Rust rule audit: clean +Rust export completeness audit: 0 candidate(s) +KSP workspace Rust rule audit: clean +Markdown table audit: clean (314 table(s), 714 file(s)) +cargo check --workspace: PASS +cargo clippy --workspace --all-targets: PASS +cargo test -p ksp-app-store-desk: PASS (37 unitaires + 6 dependency + 15 desktop contract + 14 security + 1 public API) +cargo test -p ksp-store-lib: PASS +cargo check -p ksp-store-lib --no-default-features: PASS +``` + +`pre.011` n'ajoute aucune fonctionnalité runtime. La tranche ajoute un firewall de release completeness Store Desk : + +- [X] `workspace.package.version = 0.3.8-pre.11` ; +- [X] inventaires directs Cargo exacts : build, runtime et dev dependencies Store Desk ; +- [X] inventaires npm exacts : runtime et dev dependencies, sans dépendance Select/DataTables parasite ; +- [X] inventaire exact des modules Rust `ksp-app-store-desk` ; +- [X] inventaire exact des dix commands Tauri ; +- [X] capability desktop exacte : fenêtres `splash/main`, permissions uniquement `core:default` et `tracing:default` ; +- [X] composition Config exacte `logging + store` pour `devnet/mainnet/testnet`, profil Logging `supertrace`, sans URI/credential/secret dans le composite ; +- [X] scan frontend hostile : aucun `fetch`/XHR/WebSocket/EventSource, aucun local/session storage/IndexedDB/cookie, aucun dialogue natif, aucune URI PostgreSQL/SQL/type backend physique ; +- [X] inventaire DTO visible exact et absence de `RawPageCursor`, backend physique ou source RAW bytes dans la surface IPC ; +- [X] rows Transaction/Account/Observation restent payload/data/source-byte-free ; +- [X] coexistence cursor + inspection verrouillée : exports `RawPageCursor`/`RawPageRequest` et `RawInspectionPageRequest` simultanés ; +- [X] statements PostgreSQL `LIST_*` historiques restent OFFSET-free ; +- [X] statements `INSPECT_*` restent random-access avec `OFFSET` ; +- [X] Store Desk runtime consomme `RawInspectionPageRequest` et ne consomme jamais `RawPageCursor` ; +- [X] aucun fichier Store API/façade/PostgreSQL/Config/runtime/frontend n'est modifié par `pre.011`. + +Contrôles statiques d'assemblage : audits Rust/exports/Markdown propres avant création du delta. Le gate Cargo de `pre.011` reste à rejouer par l'opérateur. +