Read the Google client secret from a command
GOOGLE_CLIENT_SECRET_COMMAND runs once at boot and its output is the
secret, so it can come from a file or 1Password instead of the
environment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LG4iASZnHb1yEM1JAASiWn
diff --git a/AGENTS.md b/AGENTS.md
index 34d08a7..1358e0a 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -85,8 +85,10 @@ fly.toml — Fly config for the main demo and PR previews
- `DATABASE_URL` — SQLite path (default: `:memory:`)
- `SESSION_SECRET` — session signing key (auto-generated if unset)
-- `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` (required) — a Google OAuth
- web client with the YouTube Data API enabled, for connecting Google.
+- `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET_COMMAND` (required) — a
+ Google OAuth web client with the YouTube Data API enabled, for
+ connecting Google. The secret is whatever the command prints, run once
+ at boot (e.g. `op read op://…` or `cat ~/.config/rank-king/secret`).
Its redirect URI is `<app URL>/settings/google/callback`. A consent
screen left in Testing mode expires refresh tokens after 7 days. The
Fly demos set placeholders in `fly.toml`, since anyone can open them.
diff --git a/Steepfile b/Steepfile
index 4ef2b3c..c928741 100644
--- a/Steepfile
+++ b/Steepfile
@@ -10,5 +10,5 @@ target :lib do
ignore "lib/rank_king/views", inline: true
signature "sig"
- library "securerandom", "net-http", "json", "uri"
+ library "securerandom", "open3", "net-http", "json", "uri"
end
diff --git a/fly.toml b/fly.toml
index 1f9ced0..4cb066b 100644
--- a/fly.toml
+++ b/fly.toml
@@ -22,7 +22,7 @@ primary_region = 'sjc'
# a demo fails at Google's consent screen.
[env]
GOOGLE_CLIENT_ID = 'demo'
- GOOGLE_CLIENT_SECRET = 'demo'
+ GOOGLE_CLIENT_SECRET_COMMAND = 'echo demo'
[processes]
app = 'rackup -o 0.0.0.0 demo.ru'
diff --git a/lib/rank_king/config.rb b/lib/rank_king/config.rb
index ffaae54..3a4204f 100644
--- a/lib/rank_king/config.rb
+++ b/lib/rank_king/config.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+require "open3"
require "securerandom"
module RankKing
@@ -22,11 +23,23 @@ module RankKing
new(
database_url: env.fetch("DATABASE_URL", ":memory:"),
session_secret: env.fetch("SESSION_SECRET") { SecureRandom.hex(64) },
- # A Google OAuth client, for connecting YouTube.
+ # A Google OAuth client, for connecting YouTube. The secret comes
+ # from a command, so it can live in a file or a password manager
+ # rather than the environment.
google_client_id: env.fetch("GOOGLE_CLIENT_ID"),
- google_client_secret: env.fetch("GOOGLE_CLIENT_SECRET"),
+ google_client_secret: secret_from(env.fetch("GOOGLE_CLIENT_SECRET_COMMAND")),
)
end
+
+ # Runs a shell command, such as `op read op://…` or `cat path`, and
+ # returns what it prints.
+ #: (String command) -> String
+ def self.secret_from(command)
+ output, status = Open3.capture2(command)
+ raise "#{command} exited with #{status.exitstatus}" unless status.success?
+
+ output.strip
+ end
end
CONFIG = Config.from_env
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 2a1783e..baf903d 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -4,4 +4,4 @@ ENV["DATABASE_URL"] = ":memory:"
# The Google client the app requires. No request reaches Google: the
# YouTube tests' app subclass hands out a fake one.
ENV["GOOGLE_CLIENT_ID"] = "client-id"
-ENV["GOOGLE_CLIENT_SECRET"] = "client-secret"
+ENV["GOOGLE_CLIENT_SECRET_COMMAND"] = "echo client-secret"