Skip to content
Merged
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
22 changes: 11 additions & 11 deletions Cargo.lock

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

7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,16 @@ maintenance requests; it is not the formal agent interface.
Config file: `~/.config/flicknote/config.json`

Environment variables:

- `FLICKNOTE_SUPABASE_URL`
- `FLICKNOTE_SUPABASE_KEY`
- `FLICKNOTE_POWERSYNC_URL`
- `FLICKNOTE_API_URL` — API Worker base URL for share links
- `FLICKNOTE_GATEWAY_URL` — Gateway origin for attachment operations and `gateway request`

`apiUrl` and `gatewayUrl` can also be set in `config.json`. After changing either
value, restart the daemon with `flicknote daemon restart`. Configure the two
endpoint values together; setting only one is rejected.

Data directory: `~/.local/share/flicknote/`

Expand Down
1 change: 1 addition & 0 deletions flicknote-cli/src/commands/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,7 @@ mod tests {
supabase_anon_key: String::new(),
powersync_url: String::new(),
api_url: String::new(),
gateway_url: String::new(),
web_url: None,
paths: ConfigPaths {
config_dir: directory.to_path_buf(),
Expand Down
1 change: 1 addition & 0 deletions flicknote-cli/src/commands/daemon_lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@ mod tests {
supabase_anon_key: String::new(),
powersync_url: String::new(),
api_url: String::new(),
gateway_url: String::new(),
web_url: None,
paths: flicknote_core::config::ConfigPaths {
config_dir: directory.path().to_path_buf(),
Expand Down
1 change: 1 addition & 0 deletions flicknote-cli/src/commands/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ mod tests {
supabase_anon_key: "key".to_string(),
powersync_url: "http://127.0.0.1:9".to_string(),
api_url: "http://127.0.0.1:9".to_string(),
gateway_url: "http://127.0.0.1:9".to_string(),
web_url: None,
paths: flicknote_core::config::ConfigPaths {
config_dir: directory.to_path_buf(),
Expand Down
1 change: 1 addition & 0 deletions flicknote-cli/src/commands/logout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ mod tests {
supabase_anon_key: String::new(),
powersync_url: String::new(),
api_url: String::new(),
gateway_url: String::new(),
web_url: None,
paths: flicknote_core::config::ConfigPaths {
config_dir: directory.to_path_buf(),
Expand Down
2 changes: 2 additions & 0 deletions flicknote-cli/src/commands/service_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ fn service_environment(config: &Config) -> Vec<(String, String)> {
"FLICKNOTE_SUPABASE_KEY",
"FLICKNOTE_POWERSYNC_URL",
"FLICKNOTE_API_URL",
"FLICKNOTE_GATEWAY_URL",
"FLICKNOTE_WEB_URL",
] {
if let Ok(value) = std::env::var(name) {
Expand Down Expand Up @@ -442,6 +443,7 @@ mod tests {
supabase_anon_key: "key".to_string(),
powersync_url: "https://sync.example".to_string(),
api_url: "https://api.example".to_string(),
gateway_url: "https://gateway.example".to_string(),
web_url: None,
paths: ConfigPaths {
config_dir: root.path().join("config/flicknote"),
Expand Down
39 changes: 22 additions & 17 deletions flicknote-cli/src/gateway.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ impl std::error::Error for GatewayRequestError {}

impl GatewayClient {
pub(crate) fn new(config: &Config) -> Result<Self, CliError> {
config.validate_gateway()?;
let http = Client::builder()
.no_proxy()
.redirect(reqwest::redirect::Policy::none())
Expand All @@ -82,7 +83,7 @@ impl GatewayClient {
&config.paths.session_file,
)
.map_err(|_| CliError::Http("Failed to configure Gateway authentication".into()))?,
gateway_origin: gateway_origin(&config.api_url)?,
gateway_origin: gateway_origin(&config.gateway_url)?,
http,
})
}
Expand Down Expand Up @@ -189,26 +190,26 @@ fn valid_retry_after(value: &str) -> Option<u64> {
}

#[cfg(test)]
fn gateway_url(api_url: &str, path: &str) -> Result<Url, CliError> {
let origin = gateway_origin(api_url)?;
fn gateway_url(gateway_url: &str, path: &str) -> Result<Url, CliError> {
let origin = gateway_origin(gateway_url)?;
gateway_path_url(&origin, path)
}

fn gateway_origin(api_url: &str) -> Result<Url, CliError> {
let api_url = Url::parse(api_url).map_err(|_| {
CliError::Other("Configured Gateway API URL is invalid; update apiUrl".into())
fn gateway_origin(gateway_url: &str) -> Result<Url, CliError> {
let gateway_url = Url::parse(gateway_url).map_err(|_| {
CliError::Other("Configured Gateway URL is invalid; update gatewayUrl".into())
})?;
if !matches!(api_url.scheme(), "http" | "https")
|| api_url.host_str().is_none()
|| !api_url.username().is_empty()
|| api_url.password().is_some()
if !matches!(gateway_url.scheme(), "http" | "https")
|| gateway_url.host_str().is_none()
|| !gateway_url.username().is_empty()
|| gateway_url.password().is_some()
{
return Err(CliError::Other(
"Configured Gateway API URL must be an HTTP(S) origin without credentials".into(),
"Configured Gateway URL must be an HTTP(S) origin without credentials".into(),
));
}

let mut origin = api_url;
let mut origin = gateway_url;
origin.set_path("/");
origin.set_query(None);
origin.set_fragment(None);
Expand Down Expand Up @@ -240,12 +241,13 @@ mod tests {
use std::net::TcpListener;
use std::thread;

fn config(api_url: String, session_file: std::path::PathBuf) -> Config {
fn config(gateway_url: String, session_file: std::path::PathBuf) -> Config {
Config {
supabase_url: "https://auth.example.test".to_string(),
supabase_anon_key: "anon-key".to_string(),
powersync_url: String::new(),
api_url,
api_url: String::new(),
gateway_url,
web_url: None,
paths: ConfigPaths {
config_dir: std::path::PathBuf::new(),
Expand Down Expand Up @@ -304,10 +306,10 @@ mod tests {

#[test]
fn gateway_url_uses_only_the_configured_origin_and_rejects_external_paths() {
let api_url = "https://dev-gw.flicknote.app/api/v1";
let configured_gateway_url = "https://dev-gw.flicknote.app";

assert_eq!(
gateway_url(api_url, "/web/v1/search?query=flicknote")
gateway_url(configured_gateway_url, "/web/v1/search?query=flicknote")
.unwrap()
.as_str(),
"https://dev-gw.flicknote.app/web/v1/search?query=flicknote"
Expand All @@ -318,7 +320,10 @@ mod tests {
"//example.com/web/v1/search",
"web/v1/search",
] {
assert!(gateway_url(api_url, path).is_err(), "accepted {path}");
assert!(
gateway_url(configured_gateway_url, path).is_err(),
"accepted {path}"
);
}
}

Expand Down
3 changes: 2 additions & 1 deletion flicknote-cli/src/main_tests/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ fn test_config(directory: &std::path::Path) -> Config {
supabase_url: "https://auth.example.test".to_string(),
supabase_anon_key: "anon-key".to_string(),
powersync_url: String::new(),
api_url: "https://gateway.example.test/api/v1".to_string(),
api_url: String::new(),
gateway_url: "https://gateway.example.test".to_string(),
web_url: Some("https://app.example".to_string()),
paths: ConfigPaths {
config_dir: directory.to_path_buf(),
Expand Down
25 changes: 17 additions & 8 deletions flicknote-cli/tests/mcp_stdio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ fn test_config(config_root: &std::path::Path, data_root: &std::path::Path) -> Co
supabase_anon_key: String::new(),
powersync_url: String::new(),
api_url: String::new(),
gateway_url: String::new(),
web_url: None,
paths: ConfigPaths {
config_file: config_dir.join("config.json"),
Expand Down Expand Up @@ -437,7 +438,8 @@ fn gateway_request_writes_a_chunked_sse_response_to_stdout_without_exposing_its_
])
.env("XDG_CONFIG_HOME", &config_root)
.env("XDG_DATA_HOME", &data_root)
.env("FLICKNOTE_API_URL", format!("{origin}/api/v1"))
.env("FLICKNOTE_API_URL", "https://api.example.test/api/v1")
.env("FLICKNOTE_GATEWAY_URL", &origin)
.output()
.unwrap();

Expand Down Expand Up @@ -475,7 +477,8 @@ fn gateway_request_forwards_piped_request_body_without_rewriting_it() {
])
.env("XDG_CONFIG_HOME", &config_root)
.env("XDG_DATA_HOME", &data_root)
.env("FLICKNOTE_API_URL", format!("{origin}/api/v1"))
.env("FLICKNOTE_API_URL", "https://api.example.test/api/v1")
.env("FLICKNOTE_GATEWAY_URL", &origin)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
Expand Down Expand Up @@ -519,7 +522,8 @@ fn gateway_request_rejects_invalid_piped_json_before_sending_it() {
])
.env("XDG_CONFIG_HOME", &config_root)
.env("XDG_DATA_HOME", &data_root)
.env("FLICKNOTE_API_URL", "http://127.0.0.1:9/api/v1")
.env("FLICKNOTE_API_URL", "https://api.example.test/api/v1")
.env("FLICKNOTE_GATEWAY_URL", "http://127.0.0.1:9")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
Expand Down Expand Up @@ -554,7 +558,8 @@ fn gateway_request_bypasses_system_proxies() {
.args(["gateway", "request", "--path", "/healthz"])
.env("XDG_CONFIG_HOME", &config_root)
.env("XDG_DATA_HOME", &data_root)
.env("FLICKNOTE_API_URL", format!("{origin}/api/v1"))
.env("FLICKNOTE_API_URL", "https://api.example.test/api/v1")
.env("FLICKNOTE_GATEWAY_URL", origin)
.env("HTTP_PROXY", &proxy)
.env("http_proxy", &proxy)
.env_remove("HTTPS_PROXY")
Expand Down Expand Up @@ -598,7 +603,8 @@ fn gateway_request_refreshes_sessions_without_using_system_proxies() {
.args(["gateway", "request", "--path", "/healthz"])
.env("XDG_CONFIG_HOME", &config_root)
.env("XDG_DATA_HOME", &data_root)
.env("FLICKNOTE_API_URL", format!("{origin}/api/v1"))
.env("FLICKNOTE_API_URL", "https://api.example.test/api/v1")
.env("FLICKNOTE_GATEWAY_URL", &origin)
.env("FLICKNOTE_SUPABASE_URL", &origin)
.env("HTTP_PROXY", &proxy)
.env("http_proxy", &proxy)
Expand Down Expand Up @@ -637,7 +643,8 @@ fn gateway_request_does_not_forward_session_refresh_to_redirect_target() {
.args(["gateway", "request", "--path", "/healthz"])
.env("XDG_CONFIG_HOME", &config_root)
.env("XDG_DATA_HOME", &data_root)
.env("FLICKNOTE_API_URL", format!("{origin}/api/v1"))
.env("FLICKNOTE_API_URL", "https://api.example.test/api/v1")
.env("FLICKNOTE_GATEWAY_URL", &origin)
.env("FLICKNOTE_SUPABASE_URL", &origin)
.output()
.unwrap();
Expand Down Expand Up @@ -674,7 +681,8 @@ fn gateway_request_does_not_echo_an_upstream_error_body() {
])
.env("XDG_CONFIG_HOME", &config_root)
.env("XDG_DATA_HOME", &data_root)
.env("FLICKNOTE_API_URL", format!("{origin}/api/v1"))
.env("FLICKNOTE_API_URL", "https://api.example.test/api/v1")
.env("FLICKNOTE_GATEWAY_URL", origin)
.output()
.unwrap();

Expand All @@ -701,7 +709,8 @@ fn gateway_request_reports_http_date_retry_after() {
.args(["gateway", "request", "--path", "/healthz"])
.env("XDG_CONFIG_HOME", &config_root)
.env("XDG_DATA_HOME", &data_root)
.env("FLICKNOTE_API_URL", format!("{origin}/api/v1"))
.env("FLICKNOTE_API_URL", "https://api.example.test/api/v1")
.env("FLICKNOTE_GATEWAY_URL", origin)
.output()
.unwrap();

Expand Down
Loading