feat: implement PKCEClient.authorize method

This commit is contained in:
ByteAtATime 2025-11-27 17:57:06 -08:00
parent 1f76b6e8ef
commit 18a46dfb72
No known key found for this signature in database
11 changed files with 153 additions and 6 deletions

1
Cargo.lock generated
View file

@ -1318,6 +1318,7 @@ dependencies = [
"serde",
"serde_json",
"tokio",
"url",
"walkdir",
"webbrowser",
"which",

View file

@ -27,6 +27,7 @@ which = "8.0.0"
rust-ini = "0.21"
freedesktop-icons = "0.4.0"
blake3 = "1.8.2"
url = "2.5.7"
# implementation ~stolen~ inspired by Tauri - we probably don't need this
[target.'cfg(windows)'.dependencies]

View file

@ -1,4 +1,5 @@
import { randomBytes, createHash } from "node:crypto";
import { randomBytes, createHash, randomUUID } from "node:crypto";
import * as protocol from "../protocol";
export enum RedirectMethod {
Web = "web",
@ -51,7 +52,14 @@ export class PKCEClient {
}> {
const { codeChallenge, codeVerifier } = generateChallenge();
const state = base64URLEncode(randomBytes(16)); // i have no idea how oauth works please help
// TODO: figure out what is required in here
const state = btoa(
JSON.stringify({
providerName: "temp value",
id: randomUUID(),
flavor: "release",
})
);
let redirectURI = "";
switch (this.options.redirectMethod) {
@ -89,4 +97,19 @@ export class PKCEClient {
toURL: () => url,
};
}
public async authorize(
options:
| {
url: string;
}
| { toURL: () => string }
): Promise<{ authorizationCode: string }> {
const url = "url" in options ? options.url : options.toURL();
const parsedUrl = new URL(url);
const state = parsedUrl.searchParams.get("state") ?? "";
return protocol.oauthAuthorize(url, state);
}
}

View file

@ -25,7 +25,8 @@ type RustRequest =
| { type: "clipboardCopy"; content: ClipboardContent; concealed: boolean }
| { type: "clipboardClear" }
| { type: "clipboardRead"; offset?: number }
| { type: "openUrl"; url: string };
| { type: "openUrl"; url: string }
| { type: "oauthAuthorize"; url: string; state: string };
type RustResponse =
| { type: "success"; result?: unknown }
@ -186,6 +187,14 @@ export const openUrl = async (url: string): Promise<void> => {
await sendRequest({ type: "openUrl", url });
};
export const oauthAuthorize = async (
url: string,
state: string
): Promise<{ authorizationCode: string }> => {
const result = await sendRequest({ type: "oauthAuthorize", url, state });
return result as { authorizationCode: string };
};
export const handleRustResponse = (data: Buffer) => {
try {
const response = unpack(data) as RustResponse & { id: number };

View file

@ -8,7 +8,7 @@ use std::{
#[cfg(windows)]
use windows_registry::{CLASSES_ROOT, CURRENT_USER, LOCAL_MACHINE};
const SCHEMES: &[&str] = &["flare", "raycast"];
pub const SCHEMES: &[&str] = &["flare", "raycast"];
pub fn get_current() -> Option<String> {
let args: Vec<String> = std::env::args().collect();
@ -22,6 +22,32 @@ pub fn get_current() -> Option<String> {
None
}
pub fn is_oauth_redirect(url: &str) -> bool {
url.starts_with("raycast://oauth") || url.starts_with("flare://oauth")
}
pub fn handle_oauth_redirect(url: &str) -> bool {
if !is_oauth_redirect(url) {
return false;
}
let url = match url::Url::parse(url) {
Ok(u) => u,
Err(_) => return false,
};
let params: std::collections::HashMap<_, _> = url.query_pairs().collect();
let state = params.get("state").map(|s| s.to_string());
let code = params.get("code").map(|c| c.to_string());
if let (Some(state), Some(code)) = (state, code) {
return crate::handlers::oauth::complete(&state, &code);
}
false
}
pub fn register_all() -> Result<(), Box<dyn std::error::Error>> {
#[cfg(windows)]
{

View file

@ -1,5 +1,6 @@
use crate::message::Message;
use crate::runtime::SidecarRuntime;
use crate::transport::Transport;
use iced::Rectangle;
use iced::futures::SinkExt;
use iced::futures::channel::mpsc;
@ -23,6 +24,9 @@ pub static LAYOUT_CACHE: LazyLock<Mutex<HashMap<usize, Rectangle>>> =
pub static CLIPBOARD: LazyLock<Mutex<Option<arboard::Clipboard>>> =
LazyLock::new(|| Mutex::new(None));
pub static OAUTH_PENDING: LazyLock<Mutex<HashMap<String, (u32, Transport)>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
pub fn send_callback(callback_id: String, value: Value) {
if let Some(mut sender) = RUNTIME_SENDER.lock().unwrap().clone() {
std::thread::spawn(move || {

View file

@ -166,3 +166,33 @@ pub mod clipboard {
.map(Some)
}
}
pub mod oauth {
use crate::transport::Transport;
use super::*;
pub fn authorize(id: u32, url: String, state: String, transport: &Transport) {
globals::OAUTH_PENDING
.lock()
.unwrap()
.insert(state, (id, transport.clone()));
let _ = crate::utils::open_url(&url);
}
pub fn complete(state: &str, code: &str) -> bool {
let entry = globals::OAUTH_PENDING.lock().unwrap().remove(state);
if let Some((id, transport)) = entry {
let response = crate::types::RustResponse::Success {
id,
result: Some(serde_json::json!({ "authorizationCode": code })),
};
let _ = transport.send(&response);
true
} else {
false
}
}
}

View file

@ -3,6 +3,7 @@ use std::os::unix::net::{UnixListener, UnixStream};
use std::path::PathBuf;
const IPC_COMMAND_TOGGLE: &[u8] = b"toggle";
const IPC_COMMAND_OAUTH_PREFIX: &[u8] = b"oauth:";
const IPC_RESPONSE_OK: &[u8] = b"ok";
fn socket_path() -> PathBuf {
@ -26,8 +27,26 @@ pub fn send_toggle() -> Result<(), Box<dyn std::error::Error>> {
}
}
pub fn send_oauth_redirect(url: &str) -> Result<(), Box<dyn std::error::Error>> {
let path = socket_path();
let mut stream = UnixStream::connect(&path)?;
let mut msg = Vec::from(IPC_COMMAND_OAUTH_PREFIX);
msg.extend_from_slice(url.as_bytes());
stream.write_all(&msg)?;
let mut response = [0u8; 16];
let n = stream.read(&mut response)?;
if &response[..n] == IPC_RESPONSE_OK {
Ok(())
} else {
Err("Unexpected response from daemon".into())
}
}
pub fn is_daemon_running() -> bool {
let path = socket_path();
println!("Checking daemon socket at: {:?}", path);
UnixStream::connect(&path).is_ok()
}
@ -46,11 +65,17 @@ where
std::thread::spawn(move || {
for stream in listener.incoming() {
if let Ok(mut stream) = stream {
let mut buf = [0u8; 64];
let mut buf = [0u8; 2048];
if let Ok(n) = stream.read(&mut buf) {
if &buf[..n] == IPC_COMMAND_TOGGLE {
let data = &buf[..n];
if data == IPC_COMMAND_TOGGLE {
on_toggle();
let _ = stream.write_all(IPC_RESPONSE_OK);
} else if data.starts_with(IPC_COMMAND_OAUTH_PREFIX) {
let url = std::str::from_utf8(&data[IPC_COMMAND_OAUTH_PREFIX.len()..])
.unwrap_or("");
crate::deep_link::handle_oauth_redirect(url);
let _ = stream.write_all(IPC_RESPONSE_OK);
}
}
}

View file

@ -45,6 +45,8 @@ use crate::view::view;
struct Cli {
#[command(subcommand)]
command: Option<Command>,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
}
#[derive(Subcommand)]
@ -107,6 +109,21 @@ fn subscription(state: &State) -> Subscription<Message> {
fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
let deep_link = if cli.args.is_empty() {
deep_link::get_current()
} else {
cli.args.first().cloned()
};
if let Some(link) = &deep_link {
if deep_link::is_oauth_redirect(link) {
if ipc::is_daemon_running() {
ipc::send_oauth_redirect(link)?;
}
return Ok(());
}
}
match cli.command {
Some(Command::Toggle) => {
if ipc::is_daemon_running() {

View file

@ -132,6 +132,11 @@ fn handle_sidecar_response(
} => (id, handlers::clipboard::copy(content, concealed)),
SidecarResponse::ClipboardClear { id } => (id, handlers::clipboard::clear()),
SidecarResponse::ClipboardRead { id, .. } => (id, handlers::clipboard::read()),
SidecarResponse::OAuthAuthorize { id, url, state } => {
handlers::oauth::authorize(id, url, state, transport);
return Ok(());
}
};
let rust_response = match result {

View file

@ -172,4 +172,10 @@ pub enum SidecarResponse {
id: u32,
url: String,
},
#[serde(rename = "oauthAuthorize")]
OAuthAuthorize {
id: u32,
url: String,
state: String,
},
}