Embed GresIQ credentials at build time via dotenvy
- Add dotenvy to dependencies and load .env in build.rs to embed GRESIQ_API_KEY and GRESIQ_API_SECRET at SDK build time. - PulseClient now reads credentials from option_env! instead of runtime env vars. - Consumer apps no longer set GresIQ credentials; only ONDE_EDGE_ID is read at runtime. - Update .gitignore to exclude .env (build-time secrets). - Update pulse module docs to reflect new credential embedding approach.
keypair34 committed
Apr 18, 2026 at 21:42 UTC
1e393a26bc52ea33b3d99f8370b54b46c67716e2
7 files changed
+74
-54
.gitignore
+3
@@ -29,3 +29,6 @@ sdk/python/.pytest_cache/
29
30
# Zed settings
31
.zed
32
+
33
+# SDK credentials (build-time secrets)
34
+.env
Cargo.lock
+7
@@ -1357,6 +1357,12 @@ dependencies = [
1357
"litrs",
1358
]
1359
1360
+[[package]]
1361
+name = "dotenvy"
1362
+version = "0.15.7"
1363
+source = "registry+https://github.com/rust-lang/crates.io-index"
1364
+checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
1365
+
1366
[[package]]
1367
name = "dtoa"
1368
version = "1.0.11"
@@ -3635,6 +3641,7 @@ version = "0.1.4"
3641
dependencies = [
3642
"anyhow",
3643
"cc",
3644
+ "dotenvy",
3645
"hf-hub",
3646
"home",
3647
"log",
Cargo.toml
+1
@@ -45,6 +45,7 @@ version = "0.3"
45
uniffi = { version = "=0.31.0", features = ["build"] }
46
# Used by build.rs to compile the tvOS ___chkstk_darwin assembly stub.
47
cc = "1"
48
+dotenvy = "0.15"
49
50
# Android: CPU-only GGUF inference via candle; also needs hf-hub and
51
# mistralrs-core for explicit cache path seeding (dirs::home_dir() panics
build.rs
+17
@@ -1,4 +1,21 @@
1
fn main() {
2
+ // ── SDK credentials ──────────────────────────────────────────────────
3
+ // Load .env from the onde crate root so GresIQ credentials are available
4
+ // to option_env!() in pulse/client.rs. Consumer apps never set these —
5
+ // they're Onde Inference's own infrastructure secrets.
6
+ println!("cargo:rerun-if-changed=.env");
7
+ let _ = dotenvy::from_filename(
8
+ std::path::Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap()).join(".env"),
9
+ );
10
+ for var in ["GRESIQ_API_KEY", "GRESIQ_API_SECRET"] {
11
+ println!("cargo:rerun-if-env-changed={var}");
12
+ if let Ok(val) = std::env::var(var) {
13
+ if !val.is_empty() {
14
+ println!("cargo:rustc-env={var}={val}");
15
+ }
16
+ }
17
+ }
18
+
19
// ── UniFFI scaffolding ───────────────────────────────────────────────
20
// Not strictly required when using `uniffi::setup_scaffolding!()` in
21
// lib.rs (the proc-macro approach), but having it here ensures the
src/inference/engine.rs
+5
-3
@@ -219,7 +219,9 @@ impl ChatEngine {
219
/// Create a new engine with no model loaded.
220
pub fn new() -> Self {
221
let environment = Self::pulse_environment();
222
- let pulse = crate::pulse::PulseClient::from_env(environment);
222
+ let edge_id = std::env::var("ONDE_EDGE_ID")
223
+ .unwrap_or_else(|_| "onde-unknown".to_string());
224
+ let pulse = crate::pulse::PulseClient::new(environment, edge_id);
225
226
match &pulse {
227
Some(_) => log::info!(
@@ -227,7 +229,7 @@ impl ChatEngine {
229
),
230
None => log::info!(
231
"ChatEngine: pulse telemetry disabled \
230
- (GRESIQ_API_KEY / GRESIQ_API_SECRET / ONDE_CLIENT_KEY / ONDE_CLIENT_SECRET not set)"
232
+ (GRESIQ_API_KEY / GRESIQ_API_SECRET not embedded at SDK build time)"
233
),
234
}
235
@@ -237,7 +239,7 @@ impl ChatEngine {
239
}
240
}
241
240
- /// Resolve the GresIQ environment from the `GRESIQ_ENVIRONMENT` env var.
242
+ /// Resolve the pulse environment from the `GRESIQ_ENVIRONMENT` env var.
243
/// Defaults to `Production` when the var is absent or unrecognised.
244
fn pulse_environment() -> smbcloud_gresiq_sdk::Environment {
245
match std::env::var("GRESIQ_ENVIRONMENT").as_deref() {
src/pulse/client.rs
+33
-35
@@ -1,12 +1,20 @@
1
-use std::collections::HashMap;
2
-
1
use smbcloud_gresiq_sdk::{Environment, GresiqClient, GresiqCredentials};
2
3
use super::events::{InferenceEvent, ModelLoadedEvent};
4
7
-/// Onde telemetry client. Wraps GresiqClient so pulse events land in the
8
-/// right GresIQ-managed tables without this crate knowing anything about
9
-/// the HTTP auth layer underneath.
5
+/// GresIQ API key embedded at SDK build time.
6
+/// Consumer apps never set this — it's Onde Inference's own credential.
7
+const EMBEDDED_API_KEY: Option<&str> = option_env!("GRESIQ_API_KEY");
8
+
9
+/// GresIQ API secret embedded at SDK build time.
10
+const EMBEDDED_API_SECRET: Option<&str> = option_env!("GRESIQ_API_SECRET");
11
+
12
+/// Onde telemetry client. Wraps GresiqClient so pulse events land in the
13
+/// right GresIQ-managed tables without consumer apps knowing anything about
14
+/// the GresIQ auth layer underneath.
15
+///
16
+/// GresIQ credentials are embedded in the SDK at build time.
17
+/// Consumer apps only provide an `edge_id` (stable device identifier).
18
///
19
/// Cheap to clone: the inner GresiqClient holds an Arc-backed reqwest::Client,
20
/// so cloning is a pointer bump, not a new TCP connection.
@@ -17,49 +25,39 @@ pub struct PulseClient {
25
}
26
27
impl PulseClient {
20
- /// Reads credentials from the environment and resolves the GresIQ
21
- /// gateway URL from the given `Environment`.
28
+ /// Build a pulse client using the GresIQ credentials embedded in the SDK.
29
///
23
- /// GresIQ layer (shared across all SDK clients):
24
- /// `GRESIQ_API_KEY`, `GRESIQ_API_SECRET`
30
+ /// Returns `None` if the SDK was compiled without `GRESIQ_API_KEY` /
31
+ /// `GRESIQ_API_SECRET` (e.g. a local dev build of onde without `.env`).
32
+ /// In that case telemetry is silently disabled — no panic, no partial state.
33
///
26
- /// SDK client layer (this app’s own credentials):
27
- /// `ONDE_CLIENT_KEY`, `ONDE_CLIENT_SECRET`
28
- /// `ONDE_EDGE_ID` (optional, defaults to `"onde-unknown"`)
29
- ///
30
- /// Returns `None` if any required var is absent. Nothing blows up —
31
- /// the engine just skips telemetry for the whole run.
32
- pub fn from_env(environment: Environment) -> Option<Self> {
33
- // GresIQ credentials
34
- let gresiq_api_key = std::env::var("GRESIQ_API_KEY").ok()?;
35
- let gresiq_api_secret = std::env::var("GRESIQ_API_SECRET").ok()?;
36
-
37
- // SDK client credentials
38
- let client_key = std::env::var("ONDE_CLIENT_KEY").ok()?;
39
- let client_secret = std::env::var("ONDE_CLIENT_SECRET").ok()?;
40
- let edge_id = std::env::var("ONDE_EDGE_ID")
41
- .unwrap_or_else(|_| "onde-unknown".to_string());
34
+ /// `edge_id` is a stable device identifier (installation UUID).
35
+ /// Pass an empty string to default to `"onde-unknown"`.
36
+ pub fn new(environment: Environment, edge_id: String) -> Option<Self> {
37
+ let api_key = EMBEDDED_API_KEY?;
38
+ let api_secret = EMBEDDED_API_SECRET?;
39
43
- let mut extra = HashMap::new();
44
- extra.insert("X-Onde-Client-Key".to_string(), client_key);
45
- extra.insert("X-Onde-Client-Secret".to_string(), client_secret);
40
+ let edge_id = if edge_id.is_empty() {
41
+ "onde-unknown".to_string()
42
+ } else {
43
+ edge_id
44
+ };
45
46
let credentials = GresiqCredentials {
48
- api_key: &gresiq_api_key,
49
- api_secret: &gresiq_api_secret,
47
+ api_key,
48
+ api_secret,
49
};
50
52
- let inner = GresiqClient::from_credentials(environment, credentials)
53
- .with_extra_headers(extra);
51
+ let inner = GresiqClient::from_credentials(environment, credentials);
52
53
Some(PulseClient { inner, edge_id })
54
}
55
56
/// Spawns a background task that writes the model-load event to the
59
- /// pulse/model_loaded table, then returns immediately. Slow network
57
+ /// pulse/model_loaded table, then returns immediately. Slow network
58
/// responses don't block the first inference request.
59
///
62
- /// Failed writes emit a warn! log line -- no retry, no queue,
60
+ /// Failed writes emit a warn! log line — no retry, no queue,
61
/// and no effect on the caller.
62
pub fn record_model_loaded(&self, model_id: String, model_name: String, load_duration_ms: u64) {
63
let client = self.clone();
@@ -77,7 +75,7 @@ impl PulseClient {
75
}
76
77
/// Same fire-and-forget pattern as record_model_loaded but for inference
80
- /// completions. Writes to pulse/inference_event. Logs on failure, no retry.
78
+ /// completions. Writes to pulse/inference_event. Logs on failure, no retry.
79
pub fn record_inference(
80
&self,
81
model_id: String,
src/pulse/mod.rs
+8
-16
@@ -1,21 +1,13 @@
1
//! Sends timing data to the Onde Inference dashboard.
2
//!
3
-//! Opt-in at the infra level, not the code level. Set the four env vars and
4
-//! telemetry starts flowing. Set none and nothing happens — no warnings,
5
-//! no stubs, the branches just never run.
3
+//! GresIQ credentials are embedded in the SDK at build time — consumer apps
4
+//! never set them. Telemetry activates automatically when the SDK was built
5
+//! with a valid `.env` file containing `GRESIQ_API_KEY` and `GRESIQ_API_SECRET`.
6
//!
7
-//! The engine reads the vars once in `ChatEngine::new()`. You can't flip
8
-//! telemetry on mid-run without restarting.
9
-//!
10
-//! | Variable | Example |
11
-//! |-----------------------|------------------------------|
12
-//! | `ONDE_BASE_URL` | `https://ondeinference.com` |
13
-//! | `ONDE_CLIENT_KEY` | `<issued by Onde Inference>` |
14
-//! | `ONDE_CLIENT_SECRET` | `<issued by Onde Inference>` |
15
-//!
16
-//! `ONDE_EDGE_ID` is what this machine gets called in the dashboard.
17
-//! Anything works — just keep it stable across restarts or you'll
18
-//! end up with duplicate edges. Defaults to `"onde-unknown"`.
7
+//! `ONDE_EDGE_ID` (read from the process environment at runtime) is what this
8
+//! machine gets called in the dashboard. Anything works — just keep it stable
9
+//! across restarts or you'll end up with duplicate edges. Defaults to
10
+//! `"onde-unknown"`.
11
12
mod client;
13
mod events;
@@ -25,7 +17,7 @@ pub use events::{InferenceEvent, ModelLoadedEvent};
17
18
use std::sync::atomic::{AtomicU64, Ordering};
19
28
-/// Cheap unique ID per inference request. No uuid crate needed.
20
+/// Cheap unique ID per inference request. No uuid crate needed.
21
/// Looks like `onde-1720000000000-42`.
22
pub(crate) fn next_request_id() -> String {
23
static COUNTER: AtomicU64 = AtomicU64::new(0);