dev: sign in as alpha@example.com without a proxy
rake dev boots dev.ru, which wraps config.ru and writes the login in,
so a browser gets through without tailscale serve tuned in.
A request that names someone keeps the name it sent,
and deployments run config.ru, where no default is lent.
Assisted-by: Claude Opus 5 via Claude Code
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
diff --git a/AGENTS.md b/AGENTS.md
index 6abba31..cdef453 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -96,8 +96,9 @@ not a fixture; edit a `.vcf` to change what the replay serves.
`DavXml`, whose methods are the only elements the server can send. Inside
an element's block, `it` is the builder, not an enclosing block's value,
so name the outer block's parameter.
-- Every request needs a `Remote-User` header or it gets a 401, so a bare
- `curl` against `rake dev` is refused until you pass it. The security of
+- Every request needs a `Remote-User` header or it gets a 401. `rake dev`
+ fills it with `alpha@example.com` when a request has none (`dev.ru`),
+ so pass it only to ask as someone else. The security of
that rests on the app being reachable only through a proxy that writes
the header itself — never bind it to anything but localhost.
- DAV exchanges that went wrong (a status of 400 or more but 401, a
diff --git a/Rakefile b/Rakefile
index 526a06e..ef30707 100644
--- a/Rakefile
+++ b/Rakefile
@@ -35,7 +35,7 @@ task :dev do
ENV["PRO_TACTS_DATA_DIR"] = data_dir.to_s
require_relative "test/fixture_data"
FixtureData.install(data_dir).close
- sh "fd -e rb . lib | entr -r rackup -o localhost"
+ sh "fd -e rb . lib | entr -r rackup -o localhost dev.ru"
end
end
diff --git a/dev.ru b/dev.ru
new file mode 100644
index 0000000..c479cc3
--- /dev/null
+++ b/dev.ru
@@ -0,0 +1,9 @@
+# config.ru for `rake dev`, which has no proxy in front of it to write
+# the login (ProTacts::DevLogin). Deployments run config.ru alone.
+require "pathname"
+
+$LOAD_PATH.unshift(Pathname.new(__dir__) / "lib")
+require "pro_tacts/dev_login"
+
+use ProTacts::DevLogin
+run Rack::Builder.parse_file(File.expand_path("config.ru", __dir__))
diff --git a/lib/pro_tacts/dev_login.rb b/lib/pro_tacts/dev_login.rb
new file mode 100644
index 0000000..27c0aa0
--- /dev/null
+++ b/lib/pro_tacts/dev_login.rb
@@ -0,0 +1,25 @@
+require "pro_tacts"
+require "pro_tacts/proxy_auth"
+
+module ProTacts
+ # Rack middleware that writes a fixed login to Remote-User, the way the
+ # proxy in front of a deployment does, so a dev server answers without
+ # one. A request that already carries the header keeps its own, so
+ # `curl -H` can still ask as someone else. Only dev.ru uses it.
+ class DevLogin
+ # @rbs @app: untyped
+
+ LOGIN = "alpha@example.com" #: String
+
+ #: (untyped app) -> void
+ def initialize(app)
+ @app = app
+ end
+
+ #: (Rack::env env) -> untyped
+ def call(env)
+ env[ProxyAuth::ENV_KEY] ||= LOGIN
+ @app.call(env)
+ end
+ end
+end
diff --git a/test/pro_tacts/test_dev_login.rb b/test/pro_tacts/test_dev_login.rb
new file mode 100644
index 0000000..d801c0b
--- /dev/null
+++ b/test/pro_tacts/test_dev_login.rb
@@ -0,0 +1,20 @@
+require_relative "../test_helper"
+
+require "pro_tacts/dev_login"
+
+class DevLoginTest < Minitest::Test
+ def login_seen(env = {})
+ seen = nil
+ app = ->(inner) { seen = ProTacts::ProxyAuth.login(inner); [200, {}, []] }
+ ProTacts::DevLogin.new(app).call(env)
+ seen
+ end
+
+ def test_a_request_with_no_login_gets_the_dev_login
+ assert_equal "alpha@example.com", login_seen
+ end
+
+ def test_a_request_naming_a_login_keeps_it
+ assert_equal "zoe@example.com", login_seen("HTTP_REMOTE_USER" => "zoe@example.com")
+ end
+end