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
4 changes: 4 additions & 0 deletions apps/desktop/src-tauri/Cargo.lock

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

6 changes: 5 additions & 1 deletion apps/desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,16 @@ twitch_oauth2 = { version = "0.17", default-features = false, features = ["reqwe
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "2"
tokio = { version = "1", features = ["macros", "time"] }
tokio = { version = "1", features = ["macros", "time", "net", "io-util", "sync"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
aho-corasick = "1.1"
arc-swap = "1.7"
rustc-hash = "2.1"
sha2 = "0.10"
base64 = "0.22"
rand = "0.9"
url = "2"

[dev-dependencies]
criterion = "0.8"
Expand Down
41 changes: 41 additions & 0 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
mod host;
mod message;
pub mod oauth_pkce;
pub mod ringbuf;
mod sidecar_commands;
mod sidecar_supervisor;
pub mod twitch_auth;
pub mod youtube_auth;

pub mod emote_index;

Expand Down Expand Up @@ -53,6 +55,11 @@ pub fn run() {
twitch_auth::commands::twitch_complete_login,
twitch_auth::commands::twitch_cancel_login,
twitch_auth::commands::twitch_logout,
youtube_auth::commands::youtube_auth_status,
youtube_auth::commands::youtube_start_login,
youtube_auth::commands::youtube_complete_login,
youtube_auth::commands::youtube_cancel_login,
youtube_auth::commands::youtube_logout,
sidecar_commands::twitch_send_message,
])
.setup(setup)
Expand Down Expand Up @@ -94,6 +101,40 @@ fn setup<R: Runtime>(app: &mut tauri::App<R>) -> Result<(), Box<dyn std::error::
);
let wakeup = Arc::new(Notify::new());
app.manage(AuthState::new(auth.clone(), wakeup.clone()));

// YouTube auth manager + state. Sidecar wiring lands separately —
// this PR only stands up the OAuth + keychain stack and the
// frontend sign-in surface; the supervisor still only knows
// about Twitch tokens. ADR 39.
{
use youtube_auth::{
AuthManager as YtAuthManager, AuthState as YtAuthState,
KeychainStore as YtKeychainStore, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET,
SCOPE_YOUTUBE, SCOPE_YOUTUBE_READONLY,
};
match reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()
{
Ok(yt_http_client) => {
let yt_auth = Arc::new(
YtAuthManager::builder(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET)
.scope(SCOPE_YOUTUBE_READONLY)
.scope(SCOPE_YOUTUBE)
.build(YtKeychainStore, yt_http_client),
);
let yt_wakeup = Arc::new(Notify::new());
app.manage(YtAuthState::new(yt_auth, yt_wakeup));
}
Err(err) => {
tracing::error!(
error = %err,
"failed to build reqwest client for YouTube; skipping YouTube auth wiring"
);
}
}
}

let sender = sidecar_commands::SidecarCommandSender::default();
app.manage(sender.clone());

Expand Down
57 changes: 57 additions & 0 deletions apps/desktop/src-tauri/src/oauth_pkce/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//! PKCE error type. Variants exist where a caller actually branches.

use thiserror::Error;

#[derive(Debug, Error)]
pub enum PkceError {
/// TCP listener could not bind `127.0.0.1:0`. Practically this means
/// the loopback interface is firewalled in a way the OS itself
/// rejects — extremely rare on a developer/streamer machine, but
/// surfaced so the UI can tell the user instead of hanging.
#[error("failed to bind loopback listener: {0}")]
Bind(#[source] std::io::Error),

/// OS RNG (`getrandom`) refused to fill the verifier/state buffer.
/// Practically unreachable on a desktop OS but surfaced rather
/// than panicked per docs/stability.md.
#[error("OS RNG unavailable: {0}")]
Rng(String),

/// Listener bound but accepting / reading the inbound HTTP request
/// failed.
#[error("loopback I/O error: {0}")]
Io(#[source] std::io::Error),

/// Inbound request did not look like the expected `GET /?...` from
/// the OAuth provider's redirect. Typically a probe (browser
/// pre-fetch, port scanner) — caller should keep waiting; the
/// listener only resolves on the *first valid* request.
#[error("malformed redirect request: {0}")]
BadRequest(&'static str),

/// Redirect carried an `error=` query parameter from the provider.
/// The caller maps this to `UserDenied` / generic `OAuth` based on
/// the value (e.g. `access_denied`).
#[error("authorization endpoint returned error: {0}")]
Authorization(String),

/// `state` parameter on the redirect did not match what we sent.
/// CSRF defense per RFC 6749 §10.12 — fail closed.
#[error("state mismatch on redirect")]
StateMismatch,

/// Provider's token endpoint rejected the exchange. Body carries
/// `error_description` when available so the user / log sees why.
#[error("token endpoint error: {0}")]
TokenEndpoint(String),

/// HTTP transport failure during code-for-token or refresh.
#[error("token endpoint HTTP error: {0}")]
Http(String),

/// Token endpoint returned 200 but the JSON didn't decode into the
/// expected shape. Usually a sign the endpoint URL is wrong or the
/// provider changed their response format.
#[error("token response decode error: {0}")]
Decode(String),
}
Loading
Loading