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.