Skip to content

Commit d55e1b8

Browse files
committed
community batch v0.4.0
1 parent 0c059d1 commit d55e1b8

File tree

27 files changed

+1750
-94
lines changed

27 files changed

+1750
-94
lines changed

Cargo.lock

Lines changed: 17 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ members = [
1818
]
1919

2020
[workspace.package]
21-
version = "0.3.49"
21+
version = "0.4.0"
2222
edition = "2021"
2323
license = "Apache-2.0 OR MIT"
2424
repository = "https://github.com/RightNow-AI/openfang"

crates/openfang-api/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ governor = { workspace = true }
3333
tokio-stream = { workspace = true }
3434
subtle = { workspace = true }
3535
base64 = { workspace = true }
36+
sha2 = { workspace = true }
37+
hmac = { workspace = true }
38+
hex = { workspace = true }
3639
socket2 = { workspace = true }
3740
reqwest = { workspace = true }
3841

crates/openfang-api/src/channel_bridge.rs

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,7 @@ impl ChannelBridgeHandle for KernelBridgeAdapter {
483483
msg
484484
}
485485

486+
#[allow(dead_code)]
486487
async fn manage_schedule_text(&self, action: &str, args: &[String]) -> String {
487488
match action {
488489
"add" => {
@@ -574,6 +575,19 @@ impl ChannelBridgeHandle for KernelBridgeAdapter {
574575
openfang_types::scheduler::CronAction::SystemEvent { text } => {
575576
text.clone()
576577
}
578+
openfang_types::scheduler::CronAction::WorkflowRun {
579+
workflow_id,
580+
input,
581+
..
582+
} => {
583+
format!(
584+
"Run workflow {workflow_id}{}",
585+
input
586+
.as_deref()
587+
.map(|i| format!(" with input: {i}"))
588+
.unwrap_or_default()
589+
)
590+
}
577591
};
578592
match self.kernel.send_message(j.agent_id, &message).await {
579593
Ok(result) => {
@@ -986,16 +1000,40 @@ fn parse_trigger_pattern(s: &str) -> Option<openfang_kernel::triggers::TriggerPa
9861000
}
9871001
}
9881002

989-
/// Read a token from an env var, returning None with a warning if missing/empty.
990-
fn read_token(env_var: &str, adapter_name: &str) -> Option<String> {
991-
match std::env::var(env_var) {
1003+
/// Resolve a token: if the value looks like an actual secret (contains `:`,
1004+
/// starts with `xoxb-`, `xapp-`, `sk-`, etc.), use it directly.
1005+
/// Otherwise treat it as an env var name and look it up.
1006+
fn read_token(env_var_or_token: &str, adapter_name: &str) -> Option<String> {
1007+
// Heuristic: actual tokens contain `:` (Telegram, Discord) or start with
1008+
// known prefixes. Env var names are uppercase ASCII identifiers.
1009+
let looks_like_token = env_var_or_token.contains(':')
1010+
|| env_var_or_token.starts_with("xoxb-")
1011+
|| env_var_or_token.starts_with("xapp-")
1012+
|| env_var_or_token.starts_with("sk-")
1013+
|| env_var_or_token.starts_with("Bearer ");
1014+
1015+
if looks_like_token {
1016+
warn!(
1017+
"{adapter_name}: config field contains what looks like an actual token \
1018+
rather than an env var name — using it directly. \
1019+
Tip: store the token in an env var and use the var name instead for security."
1020+
);
1021+
return Some(env_var_or_token.to_string());
1022+
}
1023+
1024+
match std::env::var(env_var_or_token) {
9921025
Ok(t) if !t.is_empty() => Some(t),
9931026
Ok(_) => {
994-
warn!("{adapter_name} bot token env var '{env_var}' is empty, skipping");
1027+
warn!(
1028+
"{adapter_name} token env var '{env_var_or_token}' is set but empty, skipping"
1029+
);
9951030
None
9961031
}
9971032
Err(_) => {
998-
warn!("{adapter_name} bot token env var '{env_var}' not set, skipping");
1033+
warn!(
1034+
"{adapter_name} token env var '{env_var_or_token}' not set, skipping. \
1035+
Set it with: export {env_var_or_token}=<your-token>"
1036+
);
9991037
None
10001038
}
10011039
}
@@ -1110,6 +1148,8 @@ pub async fn start_channel_bridge_with_config(
11101148
app_token,
11111149
bot_token,
11121150
sl_config.allowed_channels.clone(),
1151+
sl_config.auto_thread_reply,
1152+
sl_config.thread_ttl_hours,
11131153
));
11141154
adapters.push((adapter, sl_config.default_agent.clone()));
11151155
}

crates/openfang-api/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub mod openai_compat;
99
pub mod rate_limiter;
1010
pub mod routes;
1111
pub mod server;
12+
pub mod session_auth;
1213
pub mod stream_chunker;
1314
pub mod stream_dedup;
1415
pub mod types;

crates/openfang-api/src/middleware.rs

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,24 @@ pub async fn request_logging(request: Request<Body>, next: Next) -> Response<Bod
4343
response
4444
}
4545

46+
/// Authentication state passed to the auth middleware.
47+
#[derive(Clone)]
48+
pub struct AuthState {
49+
pub api_key: String,
50+
pub auth_enabled: bool,
51+
pub session_secret: String,
52+
}
53+
4654
/// Bearer token authentication middleware.
4755
///
4856
/// When `api_key` is non-empty (after trimming), requests to non-public
4957
/// endpoints must include `Authorization: Bearer <api_key>`.
5058
/// If the key is empty or whitespace-only, auth is disabled entirely
5159
/// (public/local development mode).
60+
///
61+
/// When dashboard auth is enabled, session cookies are also accepted.
5262
pub async fn auth(
53-
axum::extract::State(api_key): axum::extract::State<String>,
63+
axum::extract::State(auth_state): axum::extract::State<AuthState>,
5464
request: Request<Body>,
5565
next: Next,
5666
) -> Response<Body> {
@@ -114,7 +124,10 @@ pub async fn auth(
114124
|| (path == "/api/workflows" && is_get)
115125
|| path == "/api/logs/stream" // SSE stream, read-only
116126
|| (path.starts_with("/api/cron/") && is_get)
117-
|| path.starts_with("/api/providers/github-copilot/oauth/");
127+
|| path.starts_with("/api/providers/github-copilot/oauth/")
128+
|| path == "/api/auth/login"
129+
|| path == "/api/auth/logout"
130+
|| (path == "/api/auth/check" && is_get);
118131

119132
if is_public {
120133
return next.run(request).await;
@@ -123,10 +136,11 @@ pub async fn auth(
123136
// If no API key configured (empty, whitespace-only, or missing), skip auth
124137
// entirely. Users who don't set api_key accept that all endpoints are open.
125138
// To secure the dashboard, set a non-empty api_key in config.toml.
126-
let api_key = api_key.trim();
127-
if api_key.is_empty() {
139+
let api_key_trimmed = auth_state.api_key.trim().to_string();
140+
if api_key_trimmed.is_empty() && !auth_state.auth_enabled {
128141
return next.run(request).await;
129142
}
143+
let api_key = api_key_trimmed.as_str();
130144

131145
// Check Authorization: Bearer <token> header, then fallback to X-API-Key
132146
let bearer_token = request
@@ -172,6 +186,17 @@ pub async fn auth(
172186
return next.run(request).await;
173187
}
174188

189+
// Check session cookie (dashboard login sessions)
190+
if auth_state.auth_enabled {
191+
if let Some(token) = extract_session_cookie(&request) {
192+
if crate::session_auth::verify_session_token(&token, &auth_state.session_secret)
193+
.is_some()
194+
{
195+
return next.run(request).await;
196+
}
197+
}
198+
}
199+
175200
// Determine error message: was a credential provided but wrong, or missing entirely?
176201
let credential_provided = header_auth.is_some() || query_auth.is_some();
177202
let error_msg = if credential_provided {
@@ -189,6 +214,19 @@ pub async fn auth(
189214
.unwrap_or_default()
190215
}
191216

217+
/// Extract the `openfang_session` cookie value from a request.
218+
fn extract_session_cookie(request: &Request<Body>) -> Option<String> {
219+
request
220+
.headers()
221+
.get("cookie")
222+
.and_then(|v| v.to_str().ok())
223+
.and_then(|cookies| {
224+
cookies
225+
.split(';')
226+
.find_map(|c| c.trim().strip_prefix("openfang_session=").map(|v| v.to_string()))
227+
})
228+
}
229+
192230
/// Security headers middleware — applied to ALL API responses.
193231
pub async fn security_headers(request: Request<Body>, next: Next) -> Response<Body> {
194232
let mut response = next.run(request).await;

0 commit comments

Comments
 (0)