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>
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.
#