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
15 changes: 15 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,19 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Verify YouTube OAuth secrets are present
env:
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
run: |
missing=()
[ -z "$GOOGLE_CLIENT_ID" ] && missing+=(GOOGLE_CLIENT_ID)
[ -z "$GOOGLE_CLIENT_SECRET" ] && missing+=(GOOGLE_CLIENT_SECRET)
if [ ${#missing[@]} -ne 0 ]; then
echo "::error::Missing required repo secret(s): ${missing[*]}. Set them under Settings → Secrets and variables → Actions."
exit 1
fi

- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable

- uses: swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2.9.1
Expand Down Expand Up @@ -74,6 +87,8 @@ jobs:
- uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0.6.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
Comment thread
ImpulseB23 marked this conversation as resolved.
with:
projectPath: apps/desktop
tagName: ${{ github.ref_name }}
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
.env
.env.*

# Secrets — never commit OAuth client secret JSON downloads
client_secret_*.json
*.client_secret.json

# OS
.DS_Store
Thumbs.db
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src-tauri/build.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
fn main() {
// Re-run when the YouTube OAuth credentials change so cached
// builds don't bake in stale values from a previous environment.
println!("cargo:rerun-if-env-changed=GOOGLE_CLIENT_ID");
println!("cargo:rerun-if-env-changed=GOOGLE_CLIENT_SECRET");
tauri_build::build()
}
60 changes: 51 additions & 9 deletions apps/desktop/src-tauri/src/youtube_auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,28 +37,47 @@ pub use tokens::YouTubeTokens;

/// OAuth `client_id` for the registered Prismoid Google application.
///
/// Replace with the real value from your Google Cloud Console "OAuth
/// 2.0 Client IDs → Desktop app" credential before shipping. The
/// Sourced from the `GOOGLE_CLIENT_ID` env var at compile time so the
/// real Desktop client credential never lands in the public repo.
/// Falls back to a placeholder when the env var is unset *or empty*
/// (GitHub Actions expands a missing `${{ secrets.X }}` to `""`, and
/// `option_env!` returns `Some("")` in that case), in which case the
/// surrounding code paths (start_login → complete_login → exchange)
/// will all return `AuthError::OAuth("invalid_client")` until this is
/// set to a registered Desktop client.
/// all return `AuthError::OAuth("invalid_client")`.
///
/// Per RFC 8252 §8.4 and Google's own docs, this `client_id` is a
/// public identifier — it appears in browser URLs during the
/// authorization flow and is bundled in source the same way the
/// Twitch DCF flow handles `TWITCH_CLIENT_ID` (see ADR 37).
pub const GOOGLE_CLIENT_ID: &str = "REPLACE_ME.apps.googleusercontent.com";
pub const GOOGLE_CLIENT_ID: &str = or_placeholder(
option_env!("GOOGLE_CLIENT_ID"),
"REPLACE_ME.apps.googleusercontent.com",
);

/// OAuth `client_secret` for the registered Prismoid Google application.
///
/// Google issues a `client_secret` for "Desktop app" credentials and
/// requires it on the token-exchange POST, but their own
/// [installed-app docs](https://developers.google.com/identity/protocols/oauth2/native-app)
/// Sourced from the `GOOGLE_CLIENT_SECRET` env var at compile time.
/// Empty values are treated as unset (see [`GOOGLE_CLIENT_ID`] for the
/// rationale). Google issues a `client_secret` for "Desktop app"
/// credentials and requires it on the token-exchange POST, but their
/// own [installed-app docs](https://developers.google.com/identity/protocols/oauth2/native-app)
/// note: *"In this context, the client secret is obviously not treated
/// as a secret."* PKCE S256 is what cryptographically protects the
/// flow on a public client; this string is included on the wire only
/// because Google's endpoint won't accept the request without it.
pub const GOOGLE_CLIENT_SECRET: &str = "REPLACE_ME";
pub const GOOGLE_CLIENT_SECRET: &str =
or_placeholder(option_env!("GOOGLE_CLIENT_SECRET"), "REPLACE_ME");

/// Returns `env` when it's a non-empty string, otherwise `default`.
/// Used so a build env var explicitly set to `""` (the GitHub Actions
/// expansion of a missing secret) is treated as unset rather than
/// silently embedding empty credentials in the binary.
const fn or_placeholder(env: Option<&'static str>, default: &'static str) -> &'static str {
match env {
Some(v) if !v.is_empty() => v,
_ => default,
}
}

/// Google OAuth 2.0 authorization endpoint. Hard-coded to the v2
/// endpoint per Google's [installed-app guide](https://developers.google.com/identity/protocols/oauth2/native-app#step-2-send-a-request-to-googles-oauth-20-server).
Expand All @@ -78,3 +97,26 @@ pub const SCOPE_YOUTUBE_READONLY: &str = "https://www.googleapis.com/auth/youtub
/// Full YouTube scope (read + write + moderation). Required to send
/// messages and ban/timeout/delete on YouTube live chat.
pub const SCOPE_YOUTUBE: &str = "https://www.googleapis.com/auth/youtube";

#[cfg(test)]
mod tests {
use super::or_placeholder;

#[test]
fn or_placeholder_uses_env_when_non_empty() {
assert_eq!(or_placeholder(Some("value"), "fallback"), "value");
}

#[test]
fn or_placeholder_falls_back_when_unset() {
assert_eq!(or_placeholder(None, "fallback"), "fallback");
}

#[test]
fn or_placeholder_falls_back_when_empty() {
// GitHub Actions expands a missing `${{ secrets.X }}` to `""`
// and option_env! surfaces that as Some(""). The helper must
// treat it as unset so release builds don't embed empty creds.
assert_eq!(or_placeholder(Some(""), "fallback"), "fallback");
}
}
Loading