From 451f4236990026db22775402a098d0ebea92d1f0 Mon Sep 17 00:00:00 2001
From: RA <70325462+RAprogramm@users.noreply.github.com>
Date: Thu, 11 Sep 2025 18:56:19 +0700
Subject: [PATCH] feat(telegram): expand validation error coverage
---
CHANGELOG.md | 5 ++
Cargo.lock | 125 +++++++++++++++++++++++++++++
Cargo.toml | 4 +-
README.md | 11 +--
src/convert.rs | 4 +
src/convert/telegram_webapp_sdk.rs | 76 ++++++++++++++++++
src/lib.rs | 3 +
7 files changed, 222 insertions(+), 6 deletions(-)
create mode 100644 src/convert/telegram_webapp_sdk.rs
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8341cc8..3a47f16 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,8 +2,11 @@
All notable changes to this project will be documented in this file.
## [Unreleased]
+
+## [0.3.4] - 2025-09-12
### Added
- `ErrorResponse::with_retry_after_duration` helper for specifying retry advice via `Duration`.
+- Conversion from `telegram_webapp_sdk::utils::validate_init_data::ValidationError` into `AppError` (feature `telegram-webapp-sdk`).
### Changed
- `AppError::log` now includes the stable `code` field alongside `kind`.
@@ -18,6 +21,7 @@ All notable changes to this project will be documented in this file.
- Added Axum test asserting `MultipartError` becomes `AppErrorKind::BadRequest` and preserves the message.
- Expanded Actix test to check JSON body and `Retry-After`/`WWW-Authenticate` headers.
- Covered fallback classification of unknown messages as `TurnkeyErrorKind::Service`.
+- Expanded coverage of `telegram_webapp_sdk` mapping across all `ValidationError` variants.
## [0.3.3] - 2025-09-11
### Added
@@ -95,6 +99,7 @@ All notable changes to this project will be documented in this file.
- **MSRV:** 1.89
- **No unsafe:** the crate forbids `unsafe`.
+[0.3.4]: https://github.com/RAprogramm/masterror/releases/tag/v0.3.4
[0.3.3]: https://github.com/RAprogramm/masterror/releases/tag/v0.3.3
[0.3.2]: https://github.com/RAprogramm/masterror/releases/tag/v0.3.2
[0.3.1]: https://github.com/RAprogramm/masterror/releases/tag/v0.3.1
diff --git a/Cargo.lock b/Cargo.lock
index 49d41d4..e671fda 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -507,6 +507,33 @@ dependencies = [
"typenum",
]
+[[package]]
+name = "curve25519-dalek"
+version = "4.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "curve25519-dalek-derive",
+ "digest",
+ "fiat-crypto",
+ "rustc_version",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "curve25519-dalek-derive"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "darling"
version = "0.20.11"
@@ -621,6 +648,30 @@ version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
+[[package]]
+name = "ed25519"
+version = "2.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
+dependencies = [
+ "pkcs8",
+ "signature",
+]
+
+[[package]]
+name = "ed25519-dalek"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
+dependencies = [
+ "curve25519-dalek",
+ "ed25519",
+ "serde",
+ "sha2",
+ "subtle",
+ "zeroize",
+]
+
[[package]]
name = "either"
version = "1.15.0"
@@ -677,6 +728,12 @@ dependencies = [
"pin-project-lite",
]
+[[package]]
+name = "fiat-crypto"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
+
[[package]]
name = "flume"
version = "0.11.1"
@@ -876,6 +933,12 @@ dependencies = [
"digest",
]
+[[package]]
+name = "hmac-sha256"
+version = "1.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad6880c8d4a9ebf39c6e8b77007ce223f646a4d21ce29d99f70cb16420545425"
+
[[package]]
name = "home"
version = "0.5.11"
@@ -1261,6 +1324,18 @@ checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "masterror"
version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e7c3a243a6f697e05d0b971c22d0ac029b9080c20b2bbc5f4a3f43ea6024a60"
+dependencies = [
+ "http 1.3.1",
+ "serde",
+ "thiserror",
+ "tracing",
+]
+
+[[package]]
+name = "masterror"
+version = "0.3.4"
dependencies = [
"actix-web",
"axum",
@@ -1271,6 +1346,7 @@ dependencies = [
"serde",
"serde_json",
"sqlx",
+ "telegram-webapp-sdk",
"thiserror",
"tokio",
"tracing",
@@ -1833,6 +1909,15 @@ version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
[[package]]
name = "rustversion"
version = "1.0.22"
@@ -1851,6 +1936,12 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+[[package]]
+name = "semver"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
+
[[package]]
name = "serde"
version = "1.0.219"
@@ -1871,6 +1962,17 @@ dependencies = [
"typeid",
]
+[[package]]
+name = "serde-wasm-bindgen"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
+dependencies = [
+ "js-sys",
+ "serde",
+ "wasm-bindgen",
+]
+
[[package]]
name = "serde_derive"
version = "1.0.219"
@@ -2260,6 +2362,29 @@ dependencies = [
"syn",
]
+[[package]]
+name = "telegram-webapp-sdk"
+version = "0.1.0"
+source = "git+https://github.com/RAprogramm/telegram-webapp-sdk?rev=0c5a1d557e1cefe2b37a190c9359be05fe48d41a#0c5a1d557e1cefe2b37a190c9359be05fe48d41a"
+dependencies = [
+ "base64 0.21.7",
+ "ed25519-dalek",
+ "hex",
+ "hmac-sha256",
+ "js-sys",
+ "masterror 0.3.3",
+ "once_cell",
+ "percent-encoding",
+ "serde",
+ "serde-wasm-bindgen",
+ "serde_json",
+ "serde_urlencoded",
+ "thiserror",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
[[package]]
name = "thiserror"
version = "2.0.16"
diff --git a/Cargo.toml b/Cargo.toml
index 78c7536..ff5a601 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "masterror"
-version = "0.3.3"
+version = "0.3.4"
rust-version = "1.89"
edition = "2024"
description = "Application error types and response mapping"
@@ -24,6 +24,7 @@ config = ["dep:config"] # config::ConfigError -> AppError
multipart = ["axum"]
tokio = ["dep:tokio"]
reqwest = ["dep:reqwest"]
+telegram-webapp-sdk = ["dep:telegram-webapp-sdk"]
turnkey = []
openapi = ["dep:utoipa"]
@@ -54,6 +55,7 @@ config = { version = "0.15", optional = true }
utoipa = { version = "5.3", optional = true }
tokio = { version = "1", optional = true, features = ["time"] }
reqwest = { version = "0.12", optional = true, default-features = false }
+telegram-webapp-sdk = { git = "https://github.com/RAprogramm/telegram-webapp-sdk", rev = "0c5a1d557e1cefe2b37a190c9359be05fe48d41a", optional = true }
[dev-dependencies]
serde_json = "1"
diff --git a/README.md b/README.md
index f51fbb7..1153e60 100644
--- a/README.md
+++ b/README.md
@@ -169,11 +169,11 @@ utoipa = "5"
Feature flags
-- `axum` — IntoResponse
-- `actix` — ResponseError/Responder
-- `openapi` — utoipa schema
-- `serde_json` — JSON details
-- `sqlx`, `redis`, `reqwest`, `validator`, `config`, `tokio`, `multipart`
+- `axum` — IntoResponse
+- `actix` — ResponseError/Responder
+- `openapi` — utoipa schema
+- `serde_json` — JSON details
+- `sqlx`, `redis`, `reqwest`, `validator`, `config`, `tokio`, `multipart`, `telegram-webapp-sdk`
- `turnkey` — domain taxonomy and conversions for Turnkey errors
@@ -190,6 +190,7 @@ utoipa = "5"
- `validator::ValidationErrors` → Validation
- `config::ConfigError` → Config
- `tokio::time::error::Elapsed` → Timeout
+- `telegram_webapp_sdk::utils::validate_init_data::ValidationError` → TelegramAuth
diff --git a/src/convert.rs b/src/convert.rs
index c712845..52f8c42 100644
--- a/src/convert.rs
+++ b/src/convert.rs
@@ -113,6 +113,10 @@ mod tokio;
#[cfg_attr(docsrs, doc(cfg(feature = "validator")))]
mod validator;
+#[cfg(feature = "telegram-webapp-sdk")]
+#[cfg_attr(docsrs, doc(cfg(feature = "telegram-webapp-sdk")))]
+mod telegram_webapp_sdk;
+
/// Map `std::io::Error` to an internal application error.
///
/// Rationale: I/O failures are infrastructure-level and should not leak
diff --git a/src/convert/telegram_webapp_sdk.rs b/src/convert/telegram_webapp_sdk.rs
new file mode 100644
index 0000000..19d7439
--- /dev/null
+++ b/src/convert/telegram_webapp_sdk.rs
@@ -0,0 +1,76 @@
+//! Conversion from
+//! [`telegram_webapp_sdk::utils::validate_init_data::ValidationError`] into
+//! [`AppError`].
+//!
+//! Enabled with the `telegram-webapp-sdk` feature flag.
+//!
+//! ## Mapping
+//!
+//! All [`ValidationError`] variants are mapped to `AppErrorKind::TelegramAuth`
+//! and the original error text is preserved in the message.
+//!
+//! ## Rationale
+//!
+//! Failing to validate Telegram `initData` indicates an authentication problem
+//! with the incoming request. Mapping to `TelegramAuth` keeps this distinction
+//! explicit and allows callers to handle it separately from generic bad
+//! requests.
+//!
+//! ## Example
+//!
+//! ```rust
+//! # #[cfg(feature = "telegram-webapp-sdk")]
+//! # {
+//! use masterror::{AppError, AppErrorKind};
+//! use telegram_webapp_sdk::utils::validate_init_data::ValidationError;
+//!
+//! fn convert(err: ValidationError) -> AppError {
+//! err.into()
+//! }
+//!
+//! let e = convert(ValidationError::SignatureMismatch);
+//! assert!(matches!(e.kind, AppErrorKind::TelegramAuth));
+//! assert_eq!(e.message.as_deref(), Some("signature mismatch"));
+//! # }
+//! ```
+
+#[cfg(feature = "telegram-webapp-sdk")]
+use telegram_webapp_sdk::utils::validate_init_data::ValidationError;
+
+#[cfg(feature = "telegram-webapp-sdk")]
+use crate::AppError;
+
+/// Map [`ValidationError`] into an [`AppError`] with kind `TelegramAuth`.
+#[cfg(feature = "telegram-webapp-sdk")]
+#[cfg_attr(docsrs, doc(cfg(feature = "telegram-webapp-sdk")))]
+impl From for AppError {
+ fn from(err: ValidationError) -> Self {
+ AppError::telegram_auth(err.to_string())
+ }
+}
+
+#[cfg(all(test, feature = "telegram-webapp-sdk"))]
+mod tests {
+ use telegram_webapp_sdk::utils::validate_init_data::ValidationError;
+
+ use super::*;
+ use crate::AppErrorKind;
+
+ #[test]
+ fn all_variants_map_to_telegram_auth_and_preserve_message() {
+ let cases = vec![
+ ValidationError::MissingField("hash"),
+ ValidationError::InvalidEncoding,
+ ValidationError::InvalidSignatureEncoding,
+ ValidationError::SignatureMismatch,
+ ValidationError::InvalidPublicKey,
+ ];
+
+ for case in cases {
+ let msg = case.to_string();
+ let app: AppError = case.into();
+ assert!(matches!(app.kind, AppErrorKind::TelegramAuth));
+ assert_eq!(app.message.as_deref(), Some(msg.as_str()));
+ }
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index f71625a..de3cb9a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -40,6 +40,9 @@
//! - `config` — `From` mapping
//! - `tokio` — `From` mapping
//! - `reqwest` — `From` mapping
+//! - `telegram-webapp-sdk` —
+//! `From`
+//! mapping
//! - `serde_json` — support for structured JSON details in [`ErrorResponse`];
//! also pulled transitively by `axum`
//! - `multipart` — compatibility flag for Axum multipart