profile: download the mobileconfig instead of rendering it
The route already rendered it; the Rakefile did too,
with a placeholder login nobody ever knew.
Now curl takes the plist from the host it points at,
and the login in the account is the one who asked for that.

Assisted-by: Claude Opus 5 via Claude Code
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
change qmyznkrxxolvlrxkwvwrlooznwmzvkrr
commit 360bfde9b12320193fbe6c82deef3e3bd7d5ed3d
author Alpha Chen <alpha@kejadlen.dev>
date
parent ukolsluq
diff --git a/AGENTS.md b/AGENTS.md
index f0ca4b7..21adb2f 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -22,7 +22,8 @@ rake db:dump          # Write the cards, birthdays, and groups to data/dump
 rake dev              # Dev server, seeded from test/fixtures/cards into a
                       # throwaway tmpdir on every start, reloading on change
                       # (needs fd and entr)
-rake profile:install  # Render and stage carddav.mobileconfig for approval
+rake profile:install  # Download carddav.mobileconfig from the app and
+                      # stage it for approval
 ```
 
 `rake profile:*` needs `PRO_TACTS_HOSTNAME` and touches installed system
diff --git a/Rakefile b/Rakefile
index 31ba46d..526a06e 100644
--- a/Rakefile
+++ b/Rakefile
@@ -72,33 +72,25 @@ task :steep do
   sh "steep", "check"
 end
 
-desc "Render the macOS configuration profile (carddav.mobileconfig)"
-task profile: "carddav.mobileconfig"
-
-# The server ignores the username (see ProTacts::Profile.render), and a
-# profile rendered here has no request to read a login from.
-PROFILE_USERNAME = "pro-tacts@example.com"
-
-# Rebuilds when the template changes but not when the environment does
-# (PRO_TACTS_HOSTNAME, PRO_TACTS_PROFILE_NAME); delete carddav.mobileconfig
-# to force a rerender.
-file "carddav.mobileconfig" => "lib/pro_tacts/profile.rb" do |task|
-  require "pro_tacts/profile"
-
-  File.write(task.name, ProTacts::Profile.render(
-    hostname: ENV.fetch("PRO_TACTS_HOSTNAME"), username: PROFILE_USERNAME
-  ))
-end
-
 namespace :profile do
-  desc "Remove installed pro-tacts profiles, then stage a fresh one for approval"
-  task install: :remove do
-    require "pro_tacts"
-    require "pro_tacts/profile"
+  # The profile comes from the running app rather than a render here: the
+  # route is the renderer (ProTacts::Web's setup branch), it writes the
+  # requester's own login into the account where a render here had no
+  # request to read one from, and a host that answers the download is a
+  # host the account can reach. The download needs the identity header
+  # the app reads, so it works against a deployment behind a proxy that
+  # writes it and not against a server with nothing in front.
+  #
+  # Downloaded before the sweep rather than after, so a download that
+  # fails leaves the installed account where it was.
+  desc "Download the profile from PRO_TACTS_HOSTNAME and stage it for approval"
+  task :install do
+    sh "curl", "--fail", "--silent", "--show-error", "--location",
+      "--output", "carddav.mobileconfig",
+      "https://#{ENV.fetch("PRO_TACTS_HOSTNAME")}/setup/carddav.mobileconfig"
+
+    Rake::Task["profile:remove"].invoke
 
-    File.write("carddav.mobileconfig", ProTacts::Profile.render(
-      hostname: ENV.fetch("PRO_TACTS_HOSTNAME"), username: PROFILE_USERNAME
-    ))
     sh "open", "carddav.mobileconfig"
     sh "open", "x-apple.systempreferences:com.apple.preferences.configurationprofiles"
   end
diff --git a/docs/apple-contacts.md b/docs/apple-contacts.md
index a7b20e4..a717ab2 100644
--- a/docs/apple-contacts.md
+++ b/docs/apple-contacts.md
@@ -28,20 +28,26 @@ refusing a redirect) that never reach the server at all.
 ## The account setup path
 
 The fastest path is a configuration profile: `rake profile:install` (with
-`PRO_TACTS_HOSTNAME` set) first removes the installed profiles pointing at
-that host, then renders `carddav.mobileconfig`, opens it, and
-opens System Settings on the Profiles pane (via the
+`PRO_TACTS_HOSTNAME` set) downloads `carddav.mobileconfig` from that host's
+`/setup`, removes the installed profiles pointing at the same host, opens
+the download, and opens System Settings on the Profiles pane (via the
 `x-apple.systempreferences:` deep link) — the profiles CLI no longer
 supports installs, so the profile lands there as pending until you click
 Install. That click is the whole manual step. `rake profile:remove` runs
 the removal half alone via `profiles remove`.
 
-The profile carries the hostname, fixed dev credentials, and SSL —
-`CardDAVPrincipalURL` is deliberately omitted so the
+The download goes through the identity gate like any other request, so it
+works against a deployment whose proxy writes the header and not against a
+server with nothing in front of it. For one of those, fetch it with the
+header set by hand: `curl -H "Remote-User: you" -o carddav.mobileconfig
+https://host/setup/carddav.mobileconfig`.
+
+The profile carries the hostname, the requester's login, a placeholder
+password, and SSL — `CardDAVPrincipalURL` is deliberately omitted so the
 account gets an empty Server Path, exercising discovery. Every render gets a
 fresh identifier and UUIDs, so each install provisions a cold account with
 no cached sync state — that is deliberate for the experiment loop, and
-`rake profile:install` sweeping the old profiles first is what keeps it
+`rake profile:install` sweeping the old profiles before it is what keeps it
 cold. `profile:remove` finds them by scanning `profiles list` output for
 the pro-tacts prefix and a digest of `PRO_TACTS_HOSTNAME` (which it needs
 set too), so a dev sweep leaves a profile pointing at the deployment alone
diff --git a/lib/pro_tacts/web/setup.rb b/lib/pro_tacts/web/setup.rb
index 68615a0..a355e62 100644
--- a/lib/pro_tacts/web/setup.rb
+++ b/lib/pro_tacts/web/setup.rb
@@ -7,7 +7,8 @@ module ProTacts
     # provision itself: until this route existed the only renderer was
     # the Rakefile, so putting the account on a phone meant running
     # rake on the machine holding the checkout and mailing the file
-    # over. The screen and the document are separate paths because a
+    # over. It is now the only renderer — rake profile:install downloads
+    # from here. The screen and the document are separate paths because a
     # mobileconfig is not a page — the screen says what the download
     # will do, and only the link fires the install flow.
     #