Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions apps/api/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ pub struct Env {
pub sentry_dsn: Option<String>,
#[serde(default, deserialize_with = "hypr_api_env::filter_empty")]
pub posthog_api_key: Option<String>,
#[serde(default, deserialize_with = "hypr_api_env::filter_empty")]
pub supabase_service_role_key: Option<String>,
Comment on lines +18 to +19
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Duplicate supabase_service_role_key field shadows flattened SupabaseEnv, causing app startup crash

Adding supabase_service_role_key: Option<String> as a named field on Env (line 19) conflicts with the same field inside #[serde(flatten)] pub supabase: hypr_api_env::SupabaseEnv (line 22), which also contains supabase_service_role_key: String (crates/api-env/src/lib.rs:23).

Root Cause

When serde processes #[serde(flatten)], it first matches each key in the input map against explicitly-named fields. The key supabase_service_role_key matches the new top-level Option<String> field and is consumed. The remaining (unmatched) keys are then forwarded to flattened structs. Because the key has already been consumed, SupabaseEnv never sees supabase_service_role_key, and since that field is a required String (not Option), deserialization of SupabaseEnv fails.

This makes envy::from_env().expect("Failed to load environment") at apps/api/src/env.rs:55 panic on startup, crashing the application.

Impact: The API server cannot start at all. This is a complete regression.

Fix: Remove the new supabase_service_role_key field from Env and instead pass Some(env.supabase.supabase_service_role_key.clone()) in main.rs where NangoConfig::new is called, since SupabaseEnv already provides this value as a required field.

Prompt for agents
In apps/api/src/env.rs, remove lines 18-19 (the new supabase_service_role_key field). The field already exists inside the flattened SupabaseEnv struct and having it as a named field on Env shadows it, preventing SupabaseEnv from deserializing.

Then in apps/api/src/main.rs around line 74-78, change:
  let nango_config = hypr_api_nango::NangoConfig::new(
      &env.nango,
      &env.supabase,
      env.supabase_service_role_key.clone(),
  );
to:
  let nango_config = hypr_api_nango::NangoConfig::new(
      &env.nango,
      &env.supabase,
      Some(env.supabase.supabase_service_role_key.clone()),
  );

This uses the already-available supabase_service_role_key from SupabaseEnv instead of a duplicate field.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


#[serde(flatten)]
pub supabase: hypr_api_env::SupabaseEnv,
Expand Down
11 changes: 8 additions & 3 deletions apps/api/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,12 @@ async fn app() -> Router {
let auth_state_basic = AuthState::new(&env.supabase.supabase_url);
let auth_state_support = AuthState::new(&env.supabase.supabase_url);

let calendar_config = hypr_api_calendar::CalendarConfig::new(&env.nango);
let nango_config = hypr_api_nango::NangoConfig::new(&env.nango);
let nango_config = hypr_api_nango::NangoConfig::new(
&env.nango,
&env.supabase,
env.supabase_service_role_key.clone(),
);
let nango_connection_state = hypr_api_nango::NangoConnectionState::from_config(&nango_config);
let subscription_config =
hypr_api_subscription::SubscriptionConfig::new(&env.supabase, &env.stripe);
let support_config = hypr_api_support::SupportConfig::new(
Expand Down Expand Up @@ -101,8 +105,9 @@ async fn app() -> Router {

let pro_routes = Router::new()
.merge(hypr_api_research::router(research_config))
.nest("/calendar", hypr_api_calendar::router(calendar_config))
.nest("/calendar", hypr_api_calendar::router())
.nest("/nango", hypr_api_nango::router(nango_config.clone()))
.layer(axum::Extension(nango_connection_state))
.route_layer(middleware::from_fn(auth::sentry_and_analytics))
.route_layer(middleware::from_fn_with_state(
auth_state_pro,
Expand Down
4 changes: 1 addition & 3 deletions crates/api-calendar/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2024"

[dependencies]
hypr-api-env = { workspace = true }
hypr-api-nango = { workspace = true }
hypr-google-calendar = { workspace = true }
hypr-nango = { workspace = true }

Expand All @@ -13,9 +13,7 @@ chrono = { workspace = true, features = ["serde"] }
utoipa = { workspace = true }

axum = { workspace = true }
reqwest = { workspace = true, features = ["json"] }
sentry = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }

serde = { workspace = true, features = ["derive"] }
Expand Down
14 changes: 0 additions & 14 deletions crates/api-calendar/src/config.rs

This file was deleted.

4 changes: 4 additions & 0 deletions crates/api-calendar/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ pub enum CalendarError {

#[error("Internal error: {0}")]
Internal(String),

#[error(transparent)]
NangoConnection(#[from] hypr_api_nango::NangoConnectionError),
}

impl IntoResponse for CalendarError {
Expand All @@ -48,6 +51,7 @@ impl IntoResponse for CalendarError {
internal_message,
)
}
Self::NangoConnection(err) => return err.into_response(),
};

let body = Json(ErrorResponse {
Expand Down
3 changes: 0 additions & 3 deletions crates/api-calendar/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
mod config;
mod error;
mod openapi;
mod routes;
mod state;

pub use config::CalendarConfig;
pub use openapi::openapi;
pub use routes::router;
1 change: 0 additions & 1 deletion crates/api-calendar/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ use crate::routes::ListEventsResponse;
),
components(
schemas(
crate::routes::calendar::ListCalendarsRequest,
crate::routes::calendar::ListCalendarsResponse,
crate::routes::calendar::ListEventsRequest,
ListEventsResponse,
Expand Down
40 changes: 8 additions & 32 deletions crates/api-calendar/src/routes/calendar.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
use axum::{Json, extract::State};
use axum::Json;
use hypr_api_nango::{GoogleCalendar, NangoConnection};
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;

use crate::error::{CalendarError, Result};
use crate::state::AppState;

#[derive(Debug, Deserialize, ToSchema)]
pub struct ListCalendarsRequest {
pub connection_id: String,
}

#[derive(Debug, Serialize, ToSchema)]
pub struct ListCalendarsResponse {
Expand All @@ -17,7 +12,6 @@ pub struct ListCalendarsResponse {

#[derive(Debug, Deserialize, ToSchema)]
pub struct ListEventsRequest {
pub connection_id: String,
pub calendar_id: String,
#[serde(default)]
pub time_min: Option<String>,
Expand All @@ -42,7 +36,6 @@ pub struct ListEventsResponse {

#[derive(Debug, Deserialize, ToSchema)]
pub struct CreateEventRequest {
pub connection_id: String,
pub calendar_id: String,
pub summary: String,
pub start: EventDateTime,
Expand Down Expand Up @@ -82,7 +75,6 @@ pub struct CreateEventResponse {
#[utoipa::path(
post,
path = "/calendars",
request_body = ListCalendarsRequest,
responses(
(status = 200, description = "Calendars fetched", body = ListCalendarsResponse),
(status = 401, description = "Unauthorized"),
Expand All @@ -91,15 +83,9 @@ pub struct CreateEventResponse {
tag = "calendar",
)]
pub async fn list_calendars(
State(state): State<AppState>,
Json(payload): Json<ListCalendarsRequest>,
nango: NangoConnection<GoogleCalendar>,
) -> Result<Json<ListCalendarsResponse>> {
let proxy = state
.nango
.integration("google-calendar")
.connection(&payload.connection_id);
let http = hypr_nango::NangoHttpClient::new(proxy);
let client = hypr_google_calendar::GoogleCalendarClient::new(http);
let client = hypr_google_calendar::GoogleCalendarClient::new(nango.into_http());

let response = client
.list_calendars()
Expand Down Expand Up @@ -127,15 +113,10 @@ pub async fn list_calendars(
tag = "calendar",
)]
pub async fn list_events(
State(state): State<AppState>,
nango: NangoConnection<GoogleCalendar>,
Json(payload): Json<ListEventsRequest>,
) -> Result<Json<ListEventsResponse>> {
let proxy = state
.nango
.integration("google-calendar")
.connection(&payload.connection_id);
let http = hypr_nango::NangoHttpClient::new(proxy);
let client = hypr_google_calendar::GoogleCalendarClient::new(http);
let client = hypr_google_calendar::GoogleCalendarClient::new(nango.into_http());

let time_min = payload
.time_min
Expand Down Expand Up @@ -196,15 +177,10 @@ pub async fn list_events(
tag = "calendar",
)]
pub async fn create_event(
State(state): State<AppState>,
nango: NangoConnection<GoogleCalendar>,
Json(payload): Json<CreateEventRequest>,
) -> Result<Json<CreateEventResponse>> {
let proxy = state
.nango
.integration("google-calendar")
.connection(&payload.connection_id);
let http = hypr_nango::NangoHttpClient::new(proxy);
let client = hypr_google_calendar::GoogleCalendarClient::new(http);
let client = hypr_google_calendar::GoogleCalendarClient::new(nango.into_http());

let req = hypr_google_calendar::CreateEventRequest {
calendar_id: payload.calendar_id,
Expand Down
8 changes: 1 addition & 7 deletions crates/api-calendar/src/routes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,11 @@ pub(crate) mod calendar;

use axum::{Router, routing::post};

use crate::config::CalendarConfig;
use crate::state::AppState;

pub use calendar::ListEventsResponse;

pub fn router(config: CalendarConfig) -> Router {
let state = AppState::new(config);

pub fn router() -> Router {
Router::new()
.route("/calendars", post(calendar::list_calendars))
.route("/events", post(calendar::list_events))
.route("/events/create", post(calendar::create_event))
.with_state(state)
}
20 changes: 0 additions & 20 deletions crates/api-calendar/src/state.rs

This file was deleted.

2 changes: 2 additions & 0 deletions crates/api-nango/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ hypr-api-auth = { workspace = true }
hypr-api-env = { workspace = true }
hypr-nango = { workspace = true }

chrono = { workspace = true }
urlencoding = { workspace = true }
utoipa = { workspace = true }

axum = { workspace = true }
Expand Down
14 changes: 12 additions & 2 deletions crates/api-nango/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
use hypr_api_env::NangoEnv;
use hypr_api_env::{NangoEnv, SupabaseEnv};

#[derive(Clone)]
pub struct NangoConfig {
pub nango: NangoEnv,
pub supabase_url: String,
pub supabase_anon_key: String,
pub supabase_service_role_key: Option<String>,
}

impl NangoConfig {
pub fn new(nango: &NangoEnv) -> Self {
pub fn new(
nango: &NangoEnv,
supabase: &SupabaseEnv,
supabase_service_role_key: Option<String>,
) -> Self {
Self {
nango: nango.clone(),
supabase_url: supabase.supabase_url.clone(),
supabase_anon_key: supabase.supabase_anon_key.clone(),
supabase_service_role_key,
}
}
}
Loading
Loading