feat: cut over rust web main shell

This commit is contained in:
lix-2026
2026-04-29 12:24:44 +08:00
parent 7965c6c107
commit 048fe28a4d
97 changed files with 9396 additions and 1263 deletions
+76 -13
View File
@@ -2,13 +2,14 @@ use crate::app::AppConfig;
use crate::context::RequestContext;
use crate::error::WebError;
use bridge_runtime::{
RuntimeBridgeContextWire, RuntimeCommandArtifactPlan, RuntimeCommandEnvelopeWire,
RuntimeCommandExecutionPlan, RuntimeQueryExecutionPlan, build_runtime_command_artifact_plan,
build_runtime_command_artifact_plan, RuntimeBridgeContextWire, RuntimeCommandArtifactPlan,
RuntimeCommandEnvelopeWire, RuntimeCommandExecutionPlan, RuntimeQueryExecutionPlan,
};
use serde_json::{Value, json};
use serde_json::{json, Value};
use std::fs;
use std::time::Duration;
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
use base64::Engine;
const HEADER_REQUEST_ID: &str = "x-request-id";
const HEADER_TRACE_ID: &str = "x-trace-id";
@@ -17,6 +18,7 @@ const HEADER_SOURCE_CHANNEL: &str = "x-mnote-source-channel";
const HEADER_SOURCE_CLIENT: &str = "x-mnote-source-client";
const HEADER_IDEMPOTENCY_KEY: &str = "x-idempotency-key";
const COOKIE_MNOTE_WEB_CONVEX_TOKEN: &str = "mnote_web_convex_token";
const COOKIE_CONVEX_AUTH_JWT: &str = "__convexAuthJWT";
fn read_env_or_dotenv(key: &str) -> Option<String> {
if let Ok(value) = std::env::var(key) {
@@ -60,6 +62,10 @@ fn build_authorization(config: &AppConfig, context: &RequestContext) -> Result<S
return Ok(authorization.to_string());
}
if let Some(convex_token) = extract_cookie_value(context, COOKIE_CONVEX_AUTH_JWT) {
return Ok(format!("Bearer {convex_token}"));
}
if let Some(convex_token) = extract_cookie_value(context, COOKIE_MNOTE_WEB_CONVEX_TOKEN) {
return Ok(format!("Bearer {convex_token}"));
}
@@ -78,12 +84,23 @@ fn build_authorization(config: &AppConfig, context: &RequestContext) -> Result<S
.with_header("x-upstream-service", "convex")
})?;
// 说明:
// - Next 侧 `setAdminAuth(adminKey)` 走的是纯 admin auth,而不是伪造用户身份。
// - Convex 业务函数内部会在缺少真实 token 时自行回退到 DEV_USER_ID。
// - 这里若强行附带伪造 identity,会让 getAuthUserId(ctx) 命中一个未映射用户,
// 反而绕过 DEV_USER_ID fallback,导致 workspace membership 校验失败。
Ok(format!("Convex {admin_key}"))
let dev_user_id = config.dev_user_id.trim();
if dev_user_id.is_empty() {
return Ok(format!("Convex {admin_key}"));
}
// 说明:Rust Web 直接调用 Convex HTTP API 时没有 Next/Convex Auth cookie。
// 自托管开发态用 admin auth 携带 acting identity,让 @convex-dev/auth 的
// getAuthUserId(ctx) 能得到 DEV_USER_ID,从而和 Next 开发态免登录语义一致。
let identity = json!({
"subject": format!("{}|mnote-web-dev-session", dev_user_id),
"issuer": "mnote-web-dev",
"name": config.dev_user_name,
"email": config.dev_user_email,
});
let identity_encoded = base64::engine::general_purpose::STANDARD
.encode(identity.to_string().as_bytes());
Ok(format!("Convex {admin_key}:{identity_encoded}"))
}
fn extract_cookie_value(context: &RequestContext, name: &str) -> Option<String> {
@@ -459,7 +476,7 @@ fn now_iso_like() -> String {
.unwrap_or_else(|_| "1970-01-01T00:00:00Z".into())
}
async fn execute_convex_mutation_by_name(
pub async fn execute_convex_mutation_by_name(
config: &AppConfig,
context: &RequestContext,
function_name: &str,
@@ -699,12 +716,16 @@ mod tests {
use crate::app::AppConfig;
use crate::context::RequestContext;
use axum::http::{HeaderMap, HeaderValue, Method, Uri};
use base64::Engine;
fn config() -> AppConfig {
AppConfig {
service_name: "mnote-web".into(),
service_version: "0.1.0".into(),
bind_addr: "127.0.0.1:0".into(),
public_bind_addr: "127.0.0.1:3000".into(),
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
enable_legacy_next_compat: true,
enable_debug_shell_routes: false,
hermes_base_path: "/api/hermes".into(),
compat_next_base_path: "/api/compat/next".into(),
@@ -744,11 +765,21 @@ mod tests {
}
#[test]
fn build_authorization_falls_back_to_plain_admin_auth() {
fn build_authorization_falls_back_to_dev_admin_identity() {
let authorization = build_authorization(&config(), &request_context(HeaderMap::new()))
.expect("authorization");
assert_eq!(authorization, "Convex admin-demo");
assert!(authorization.starts_with("Convex admin-demo:"));
let encoded = authorization
.trim_start_matches("Convex admin-demo:")
.trim();
let decoded = base64::engine::general_purpose::STANDARD
.decode(encoded)
.expect("identity base64");
let identity: serde_json::Value = serde_json::from_slice(&decoded).expect("identity json");
assert_eq!(identity["subject"], "dev-user|mnote-web-dev-session");
assert_eq!(identity["issuer"], "mnote-web-dev");
assert_eq!(identity["email"], "dev@mnote.local");
}
#[test]
@@ -766,4 +797,36 @@ mod tests {
assert_eq!(authorization, "Bearer token-from-cookie");
}
#[test]
fn build_authorization_prefers_convex_auth_jwt_over_legacy_handoff_cookie() {
let mut headers = HeaderMap::new();
headers.insert(
"cookie",
HeaderValue::from_static(
"mnote_web_convex_token=legacy-token; __convexAuthJWT=jwt-from-convex-auth",
),
);
let authorization =
build_authorization(&config(), &request_context(headers)).expect("authorization");
assert_eq!(authorization, "Bearer jwt-from-convex-auth");
}
#[test]
fn build_authorization_reads_convex_auth_jwt_cookie() {
let mut headers = HeaderMap::new();
headers.insert(
"cookie",
HeaderValue::from_static(
"foo=bar; __convexAuthJWT=jwt-from-convex-auth; __convexAuthRefreshToken=refresh",
),
);
let authorization =
build_authorization(&config(), &request_context(headers)).expect("authorization");
assert_eq!(authorization, "Bearer jwt-from-convex-auth");
}
}