From 185aa3168e86ded6c061a3d509fe473f944d3b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Thu, 27 Aug 2026 05:46:57 +0000 Subject: [PATCH] two todos from the caldav extraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit setting up a phone should be one link inside officer — open Calendar, the screen already knows this phone is not set up, click, done. the endpoint for it exists (POST /api/caldav/provision/ios) and nothing in the UI calls it, so the smooth path is built and hidden while the path a new person walks has four fields. it also does not currently work: the profile is refused by iOS 26.6 as "Invalid Profile". the todo records what was RULED OUT with evidence rather than reasoning, because that is the expensive part to redo — delivery works, the phone really received it (the claim is logged now), the server side is correct, and removing PayloadScope did not fix it. signing is untested and is the first experiment. second: a plugin with a sidecar proxy loses the request body on its own routes. createSidecarProxy registers the whole mount prefix and bodyParser skips it, which is right for forwarded traffic and load-bearing for uploads — but it covers the plugin's own routes too. caldav works around it locally; the real fix is the proxy claiming only the sub-path it serves, which is shared machinery and not a thing to change with a phone waiting. --- ...-up-a-phone-should-be-a-link-in-the-app.md | 64 +++++++++++++++++++ ...in-s-own-routes-lose-their-request-body.md | 38 +++++++++++ 2 files changed, 102 insertions(+) create mode 100644 todo/mobile-apps/setting-up-a-phone-should-be-a-link-in-the-app.md create mode 100644 todo/plugin-infrastructure/a-plugin-s-own-routes-lose-their-request-body.md diff --git a/todo/mobile-apps/setting-up-a-phone-should-be-a-link-in-the-app.md b/todo/mobile-apps/setting-up-a-phone-should-be-a-link-in-the-app.md new file mode 100644 index 00000000..24608d04 --- /dev/null +++ b/todo/mobile-apps/setting-up-a-phone-should-be-a-link-in-the-app.md @@ -0,0 +1,64 @@ +--- +id: mobile-apps/setting-up-a-phone-should-be-a-link-in-the-app +title: 'Setting up a phone for calendar and contacts should be one link inside Officer, not a manual account form' +theme: mobile-apps +status: open +created: 2026-08-27 +date_source: stated +order: 5 +--- + +**The owner's shape for it, stated 2026-08-27:** open Calendar or Contacts in Officer, and the screen +already knows this phone is not set up. One link. Clicking it generates the profile on the server, the +phone installs it, and everything works. No typing a server address, no username, no password. + +The endpoint for that already exists — `POST /api/caldav/provision/ios` in the caldav plugin — and it is +**unreachable from the product**: nothing in the UI calls it. The "Sync to your phone" modal offers only +manual app passwords. So the smooth path is built and hidden, and the path a new person actually walks is +the one with four fields in it. + +## What is known, from an evening of it on 2026-08-27 + +**The profile does not install on iOS 26.6.** Every attempt returned *Profile Error — Invalid Profile*. +What was ruled out, each with evidence rather than reasoning: + +- **Delivery works.** Fetched over the public URL from another machine: `200`, + `application/x-apple-aspen-config`, 2,491 bytes, parses cleanly with `plistlib`. +- **The phone really received it.** `claimIosProfile` now logs, and it logged + `ios profile claimed — 0 still pending` on the attempt that failed. iOS rejected the CONTENTS. +- **The server side is correct.** The principal advertises `addressbook-home-set`, `/1/contacts/` is + typed as an addressbook, and the manually-added accounts sync fine against the same URLs. +- **`PayloadScope` was removed** (Apple documents it as macOS-only). It did not fix it. Worth stating + plainly because it was briefly believed to have — the calendar had started working at the same moment + for an unrelated reason: the owner had added the account by hand. + +So the remaining suspect is the payload itself — some key iOS 26 refuses, or unsigned profiles being +rejected outright, which would make `DAV_PROFILE_SIGN_CERT`/`KEY` a precondition rather than a nicety. +**Nothing has been tested against a signed profile.** That is the first experiment. + +## The second problem, and it does not go away with a working profile + +**iOS folds CardDAV into an existing CalDAV account with the same server and username.** Adding the +calendar by hand produced one "Officer" account offering Calendars and Reminders, with no Contacts toggle +anywhere — and the server confirmed nothing had ever requested `/dav/1/contacts/`. The workaround that +worked was adding CardDAV as a separate account with a DIFFERENT description. + +`ios-profile.ts` already carries a comment claiming a single two-payload profile sidesteps this. On +26.6 it does not, and the comment should be corrected when this is picked up rather than trusted. + +A `kinds` option was added to `buildPlist` and `only: 'caldav' | 'carddav'` to the route so the two +accounts can be split across two profiles — written, never verified, because the profile itself never +installed. + +## What "already knows this phone is not set up" needs + +Nothing exists for it today. The server can see whether a device has ever authenticated — `lastUsedAt` on +`dav_app_passwords` — which is enough for "no phone has ever synced", but not for "THIS phone has not", +since a browser cannot identify the device it is being read on. Worth deciding whether the weaker signal +is enough before building the stronger one. + +## Related + +- `plugin-infrastructure/a-plugin-s-own-routes-lose-their-request-body` — found while debugging this. +- The manual path works and is what the owner is using: app password from the modal, then CalDAV and + CardDAV added by hand as two accounts with different descriptions. diff --git a/todo/plugin-infrastructure/a-plugin-s-own-routes-lose-their-request-body.md b/todo/plugin-infrastructure/a-plugin-s-own-routes-lose-their-request-body.md new file mode 100644 index 00000000..de5ddf18 --- /dev/null +++ b/todo/plugin-infrastructure/a-plugin-s-own-routes-lose-their-request-body.md @@ -0,0 +1,38 @@ +--- +id: plugin-infrastructure/a-plugin-s-own-routes-lose-their-request-body +title: "A plugin that has a sidecar proxy loses the request body on its OWN routes" +theme: plugin-infrastructure +status: open +created: 2026-08-27 +date_source: stated +order: 9 +--- + +**`createSidecarProxy` registers the plugin's whole mount prefix with `registerProxiedPrefix`, and +`bodyParser` then skips every request under it — including routes the plugin serves itself.** Those +routes get `ctx.get('body') === {}` and every field reads as missing. + +Found on 2026-08-27 extracting caldav: `POST /api/caldav/provision/ios` answered +`{"error":"deviceLabel is required"}` for a request that carried one. It had never happened before +because app passwords lived at `/api/dav` — a second top-level prefix that was not proxied — and one +prefix per plugin is the rule that ended that arrangement. + +**The skip itself is correct and must not be removed.** Parsing a body that is only passing through is +wasted for JSON and *corrupting* for `multipart/form-data`: Hono caches the parsed `FormData` and +re-serialises it with a NEW boundary while the proxy forwards the ORIGINAL content-type header naming +the old one, so every upload fails with "Multipart: Unexpected end of form". That is why the mechanism +exists. + +**The narrow fix is that the proxy should register only the sub-path it actually serves.** Caldav's +sidecar answers `_officer/*` and nothing else, so `/api/caldav/_officer` is the honest claim; the rest of +the prefix is the plugin's own and should be parsed normally. That means `createSidecarProxy` taking the +proxied sub-path rather than assuming the whole mount. + +Not done on the night it was found: it is shared machinery every sidecar depends on, and the change was +diagnosed while a phone was waiting. **The caldav plugin works around it locally** — `jsonBody()` in +`api/app-passwords.ts` reads `ctx.req.json()` directly, with the reasoning inline. + +**Who else is affected:** every plugin that mixes its own routes with a sidecar proxy under one prefix, +which is most of them. They have not hit it because their own routes are GETs, or because their writes go +to the sidecar. A plugin adding its first non-proxied POST will hit it, and the symptom — a required +field reported missing on a request that plainly has it — points at the caller rather than at this.