toolkit-zero
A feature-selective Rust utility toolkit. Pull in only the modules you need via Cargo feature flags — each feature compiles exactly what it requires and nothing more.
Table of Contents
Overview
toolkit-zero is designed to be zero-waste: you declare only the features you want and cargo compiles only what those features require. There is no "kitchen sink" import.
Feature flags
| Feature | What it enables | Module exposed |
|---|---|---|
serialization |
VEIL cipher — seal any struct to opaque bytes and back | toolkit_zero::serialization |
socket-server |
Typed HTTP server builder (includes serialization) |
toolkit_zero::socket::server |
socket-client |
Typed HTTP client builder (includes serialization) |
toolkit_zero::socket::client |
socket |
Both socket-server and socket-client |
both socket sub-modules |
location-native |
Browser-based geolocation (includes socket-server) |
toolkit_zero::location::browser |
location |
Alias for location-native |
toolkit_zero::location |
enc-timelock-keygen-now |
Time-lock key derivation from the system clock | toolkit_zero::encryption::timelock |
enc-timelock-keygen-input |
Time-lock key derivation from a caller-supplied time | toolkit_zero::encryption::timelock |
enc-timelock-async-keygen-now |
Async variant of enc-timelock-keygen-now |
toolkit_zero::encryption::timelock |
enc-timelock-async-keygen-input |
Async variant of enc-timelock-keygen-input |
toolkit_zero::encryption::timelock |
encryption |
All four enc-timelock-* features |
toolkit_zero::encryption::timelock |
backend-deps |
Re-exports all third-party deps used by each active module | *::backend_deps |
Add to Cargo.toml:
[]
# VEIL cipher only
= { = "3.2", = ["serialization"] }
# HTTP server only
= { = "3.2", = ["socket-server"] }
# HTTP client only
= { = "3.2", = ["socket-client"] }
# Both sides
= { = "3.2", = ["socket"] }
# Geolocation (pulls in socket-server automatically)
= { = "3.2", = ["location"] }
# Full time-lock encryption suite
= { = "3.2", = ["encryption"] }
# Re-export deps alongside socket-server
= { = "3.2", = ["socket-server", "backend-deps"] }
Serialization
Feature: serialization
The VEIL cipher converts any bincode-encodable struct into an opaque, key-dependent byte blob. The output has no recognisable structure and every output byte depends on the full input and the key. Without the exact key the bytes cannot be inverted.
Entry points:
| Function | Direction |
|---|---|
toolkit_zero::serialization::seal(&value, key) |
struct → Vec<u8> |
toolkit_zero::serialization::open::<T>(&bytes, key) |
Vec<u8> → struct |
key is Option<&str>. Pass None to use the built-in default key.
Your types must derive Encode and Decode:
use ;
// With the default key
let cfg = Config ;
let blob = seal.unwrap;
let back: Config = open.unwrap;
assert_eq!;
// With a custom shared key
let blob2 = seal.unwrap;
let back2: Config = open.unwrap;
assert_eq!;
Socket — server
Feature: socket-server
A fluent builder API for declaring typed HTTP routes and serving them. Every route starts from ServerMechanism, is enriched with optional body / query / state expectations, and is finalised with .onconnect(async_handler). Finalised routes are registered on a Server, which is then served with a single .await.
Plain routes
No body and no query. The handler receives nothing.
use ;
let mut server = default;
server.mechanism;
All seven HTTP methods are available: get, post, put, delete, patch, head, options.
JSON body routes
Call .json::<T>() on the mechanism. The JSON body is deserialised before the handler runs; the handler receives a ready-to-use T. T must implement serde::Deserialize. A missing or malformed body returns 400 Bad Request automatically.
use Deserialize;
use ;
let mut server = default;
server.mechanism;
Query parameter routes
Call .query::<T>() on the mechanism. When a request arrives, warp deserialises
the URL query string into T before calling the handler — the handler receives a
ready-to-use value. T must implement serde::Deserialize.
URL shape the server expects:
GET /search?q=hello&page=2
Each field of T maps to one key=value pair. Nested structs are not supported
by serde_urlencoded; keep query types flat.
use Deserialize;
use ;
let mut server = default;
server.mechanism;
Missing or malformed query parameters cause warp to return 400 Bad Request
before the handler is invoked.
Shared state
Call .state(value) on the mechanism. A fresh clone of the state is injected into every request. The state must be Clone + Send + Sync + 'static. Wrap mutable state in Arc<Mutex<_>> or Arc<RwLock<_>>.
use ;
use Serialize;
use ;
let store: = new;
let mut server = default;
server.mechanism;
Combining state with body / query
State and a body (or query) can be combined. The order of .state() and .json() / .query() does not matter. The handler receives (state: S, body_or_query: T).
use ;
use ;
use ;
let store: = new;
let mut server = default;
server.mechanism;
VEIL-encrypted routes
Call .encryption::<T>(key) (body) or .encrypted_query::<T>(key) (query) on the mechanism. Provide a SerializationKey::Default (built-in key) or SerializationKey::Value("your-key") (custom key).
Before the handler is called, the body or query is VEIL-decrypted using the supplied key. A wrong key, mismatched secret, or corrupt payload returns 403 Forbidden without ever reaching the handler. The T the closure receives is always a trusted, fully-decrypted value.
T must implement bincode::Decode<()>.
use ;
use SerializationKey;
use ;
let mut server = default;
server.mechanism;
For encrypted query parameters, the client sends ?data=<base64url> where the value is URL-safe base64 of the VEIL-sealed struct bytes.
Serving the server
// Bind to a specific address — runs until the process exits
server.serve.await;
Note: Routes are evaluated in registration order — the first matching route wins.
serve(),serve_with_graceful_shutdown(), andserve_from_listener()all panic immediately if called on aServerwith no routes registered.
Graceful shutdown
use oneshot;
let = ;
// Shut down later by calling: tx.send(()).ok();
server.serve_with_graceful_shutdown.await;
To use an OS-assigned port (e.g. to know the port before the server starts):
use TcpListener;
use oneshot;
let listener = bind.await?;
let port = listener.local_addr?.port;
let = ;
server.serve_from_listener.await;
Building responses
Use the reply! macro:
| Expression | Result |
|---|---|
reply!() |
200 OK with empty body |
reply!(json => value) |
200 OK with JSON-serialised body |
reply!(json => value, status => Status::Created) |
201 Created with JSON body |
reply!(message => warp::reply(), status => Status::NoContent) |
custom status on any reply |
reply!(sealed => value) |
200 OK with VEIL-sealed body (application/octet-stream) |
reply!(sealed => value, key => SerializationKey::Value("k")) |
sealed with explicit key |
Status re-exports the most common HTTP status codes as named variants (Status::Ok, Status::Created, Status::NoContent, Status::BadRequest, Status::Forbidden, Status::NotFound, Status::InternalServerError).
Sync handlers
Every route finaliser (onconnect) has an unsafe blocking counterpart — onconnect_sync — for cases where an existing blocking API cannot easily be made async. Not recommended for production traffic.
use ;
let mut server = default;
// SAFETY: handler is fast; no shared mutable state; backpressure applied externally
unsafe
unsafe is required because onconnect_sync dispatches to Tokio's blocking thread pool, which carries important caveats:
- The pool caps live OS threads at 512 (default), but the waiting-task queue is unbounded. Under a traffic surge, tasks accumulate without limit, leading to OOM or severe latency before any queued task executes.
- Any panic inside the handler is silently converted to a
Rejection, masking runtime errors. - When the handler holds a lock (e.g.
Arc<Mutex<_>>), lock contention across concurrent blocking tasks can stall the thread pool indefinitely.
onconnect_sync is available on every builder variant: plain, .json, .query, .state, and their combinations. All have identical safety requirements.
Socket — client
Feature: socket-client
A fluent builder API for issuing typed HTTP requests. Construct a Client from a Target, pick an HTTP method, optionally attach a body or query parameters, and call .send().await or .send_sync().
Creating a client
use ;
// Async-only — safe to create inside #[tokio::main]
let client = new_async;
// Sync-only — must be created before entering any async runtime
let client = new_sync;
// Both async and blocking — must be created before entering any async runtime
let client = new;
// Remote target
let client = new_async;
| Constructor | .send() async |
.send_sync() blocking |
Safe inside #[tokio::main] |
|---|---|---|---|
Client::new_async(target) |
✓ | ✗ — panics at call site | ✓ |
Client::new_sync(target) |
✗ — panics at call site | ✓ | ✗ — panics at construction |
Client::new(target) |
✓ | ✓ | ✗ — panics at construction |
Why
Client::new()andClient::new_sync()panic inside an async context:reqwest::blocking::Clientcreates its own single-threaded Tokio runtime internally. Tokio does not allow a runtime to start while another is already running on the same thread.Client::new()proactively detects this viatokio::runtime::Handle::try_current()and panics at construction time with an actionable message before any field is initialised.Client::new_sync()fails the same way throughreqwestduring construction.Rule of thumb:
- Async program (
#[tokio::main]) → useClient::new_async().- Sync program with no runtime → use
Client::new_sync()orClient::new().- Mixed program (sync
main, manualtokio::Runtime) → build theClientbefore starting the runtime.
Plain requests
use Deserialize;
// Async
let item: Item = client.get.send.await?;
// Sync
let item: Item = client.get.send_sync?;
All seven HTTP methods are available: get, post, put, delete, patch, head, options.
JSON body requests
Attach a body with .json(value). value must implement serde::Serialize. The response is deserialised as R: serde::Deserialize.
use ;
let created: Item = client
.post
.json
.send
.await?;
Query parameter requests
Attach query parameters with .query(value). value must implement
serde::Serialize. The fields are serialised by serde_urlencoded and
appended to the request URL as ?key=value&....
URL the client will send:
GET /items?status=active&page=1
use ;
// Sends: GET /items?status=active&page=1
let items: = client
.get
.query
.send
.await?;
Field order in the URL is determined by struct field declaration order. Keep
query structs flat — nested structs are not supported by serde_urlencoded.
VEIL-encrypted requests
Attach a VEIL-sealed body with .encryption(value, key). The body is sealed before the wire send and the response bytes are opened automatically. Both value (request) and R (response) use bincode — value: T must implement bincode::Encode, R must implement bincode::Decode<()>.
use ;
use SerializationKey;
use ClientError;
let resp: Resp = client
.post
.encryption
.send
.await?;
For encrypted query parameters, use .encrypted_query(value, key). The params are sealed and sent as ?data=<base64url>.
let resp: Resp = client
.get
.encrypted_query
.send
.await?;
Both .send() and .send_sync() are available on encrypted builders, returning Result<R, ClientError>.
Sync vs async sends
| Method | Blocks the thread | Requires constructor |
|---|---|---|
.send().await |
No | Client::new_async() or Client::new() |
.send_sync() |
Yes | Client::new_sync() or Client::new() |
Using the wrong variant panics at the call site with an explicit message pointing to the correct constructor:
- Calling
.send()on anew_sync()client →"Client was created with new_sync() — call new_async() or new() to use async sends" - Calling
.send_sync()on anew_async()client →"Client was created with new_async() — call new_sync() or new() to use sync sends"
These call-site panics are distinct from the construction-time panic that Client::new() (and Client::new_sync()) raises when constructed inside an active Tokio runtime — see Creating a client.
Location
Feature: location (or location-native)
Acquires the device's geographic coordinates by opening the system's default browser to a locally served consent page. The browser prompts the user for location permission via the standard Web Geolocation API. On success, the coordinates are POSTed back to the local server, which shuts itself down and returns the result to the caller.
No external service is contacted. Everything happens on 127.0.0.1.
Blocking usage
Works from synchronous main and from inside an async Tokio runtime. When called inside an existing runtime, an OS thread is spawned to avoid nesting runtimes.
use ;
match __location__
Async usage
Preferred when already inside a Tokio async context — avoids the extra OS thread spawn.
use ;
async
Page templates
PageTemplate controls what the user sees in the browser.
| Variant | Description |
|---|---|
PageTemplate::Default { title, body_text } |
Clean single-button consent page. Both fields are Option<String> and fall back to built-in text when None. |
PageTemplate::Tickbox { title, body_text, consent_text } |
Same as Default but adds a checkbox the user must tick before the button activates. |
PageTemplate::Custom(html) |
Fully custom HTML string. Place exactly one {} where the capture button should appear; the required JavaScript is injected automatically. |
use ;
// Custom title only
let _data = __location__;
// Tick-box consent
let _data = __location__;
// Fully custom HTML
let html = r#"<!DOCTYPE html>
<html><body>
<h1>Grant access</h1>
{}
</body></html>"#;
let _data = __location__;
LocationData fields
| Field | Type | Description |
|---|---|---|
latitude |
f64 |
Decimal degrees (WGS 84) |
longitude |
f64 |
Decimal degrees (WGS 84) |
accuracy |
f64 |
Horizontal accuracy in metres (95 % confidence) |
altitude |
Option<f64> |
Metres above WGS 84 ellipsoid, if available |
altitude_accuracy |
Option<f64> |
Accuracy of altitude in metres, if available |
heading |
Option<f64> |
Degrees clockwise from true north [0, 360), or None if stationary |
speed |
Option<f64> |
Ground speed in m/s, or None if unavailable |
timestamp_ms |
f64 |
Browser Unix timestamp in milliseconds |
LocationError variants
| Variant | Cause |
|---|---|
PermissionDenied |
User denied the browser's location permission prompt |
PositionUnavailable |
Device cannot determine its position |
Timeout |
No fix within the browser's built-in 30 s timeout |
ServerError |
Failed to start the local HTTP server or Tokio runtime |
Encryption — Timelock
Feature: encryption (or any enc-timelock-* sub-feature)
Derives a 32-byte time-locked key through a three-pass RAM-hard KDF chain:
Argon2id (pass 1) → scrypt (pass 2) → Argon2id (pass 3)
The key is only reproducible at the right time with the right salts. Paired with a passphrase (joint KDF), the search space becomes time-window × passphrase-space — extremely expensive to brute-force.
Timelock features
| Feature | Sync/Async | Entry point | Path |
|---|---|---|---|
enc-timelock-keygen-input |
sync | timelock(…, None) |
Encryption — derive from explicit time |
enc-timelock-keygen-now |
sync | timelock(…, Some(p)) |
Decryption — derive from system clock + header |
enc-timelock-async-keygen-input |
async | timelock_async(…, None) |
Async encryption |
enc-timelock-async-keygen-now |
async | timelock_async(…, Some(p)) |
Async decryption |
encryption |
both | both entry points | All four paths |
KDF presets
KdfPreset provides named parameter sets calibrated per platform:
| Preset | Peak RAM | Platform / intended use |
|---|---|---|
Fast / FastX86 |
~128 MiB | Cross-platform / x86-64 dev & CI |
FastArm |
~256 MiB | Linux ARM64 dev & CI |
FastMac |
~512 MiB | macOS (Apple Silicon) dev & CI |
Balanced / BalancedX86 |
~512 MiB | Cross-platform / x86-64 production |
BalancedArm |
~512 MiB | Linux ARM64 production |
BalancedMac |
~1 GiB | macOS (Apple Silicon) production |
Paranoid / ParanoidX86 / ParanoidArm |
~768 MiB | Cross-platform / x86-64 / ARM64 max security |
ParanoidMac |
~3 GiB | macOS max security (requires 8+ GiB unified memory) |
Custom(KdfParams) |
user-defined | Fully manual — tune to your hardware |
Timelock usage
use *;
// ── Encryption side ── caller sets the unlock time ─────────────────────────
let salts = generate;
let kdf = BalancedMac.params; // ~2 s on M2
let at = new.unwrap;
// params = None → _at (encryption) path
let enc_key = timelock.unwrap;
// Pack all settings — including salts and KDF params — into a self-contained
// header. Salts and KDF params are not secret; store the header in plaintext
// alongside the ciphertext so the decryption side can reconstruct the key.
let header = pack;
// ── Decryption side ── re-derives from the live clock ───────────────────────
// Load header from ciphertext; call at 14:30 local time.
// params = Some(header) → _now (decryption) path
let dec_key = timelock.unwrap;
assert_eq!;
For async usage replace timelock with timelock_async and .await the result.
All arguments are taken by value. Requires the matching enc-timelock-async-keygen-*
feature(s).
Backend deps
Feature: backend-deps
When combined with any other feature, backend-deps adds a backend_deps sub-module to every active module. Each backend_deps module re-exports (via pub use) every third-party crate that its parent uses internally.
This lets downstream crates access those dependencies without declaring them separately in their own Cargo.toml.
| Module | Path | Re-exports |
|---|---|---|
serialization |
toolkit_zero::serialization::backend_deps |
bincode, base64 |
socket (server side) |
toolkit_zero::socket::backend_deps |
bincode, base64, serde, tokio, log, bytes, serde_urlencoded, warp |
socket (client side) |
toolkit_zero::socket::backend_deps |
bincode, base64, serde, tokio, log, reqwest |
location |
toolkit_zero::location::backend_deps |
tokio, serde, webbrowser, rand |
encryption (timelock) |
toolkit_zero::encryption::timelock::backend_deps |
argon2, scrypt, zeroize, chrono, rand; tokio (async variants only) |
Each re-export inside backend_deps is individually gated on its parent feature, so only the deps that are actually compiled appear. Enabling backend-deps alone (without any other feature) compiles cleanly but exposes nothing.
# Example: socket-server + dep re-exports
= { = "3.2", = ["socket-server", "backend-deps"] }
Then in your code:
// Access warp directly through toolkit-zero
use warp;
// Access bincode through serialization
use bincode;
License
MIT — see LICENSE.