Files
platform/docs/mobile-dav-provisioning-feedback.md
T
pastilhasandClaude Opus 5 0799ec7325 commit the mobile dav correspondence
the letter that asked for the provisioning endpoint and the reply that answers
it. both lived only in an untracked COMMS/ directory on the dev box, which is
where the reasoning behind an api contract goes to be lost.

the feedback comes with it: a reply that answers a letter nobody can read is
half a record.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 14:30:25 +00:00

7.0 KiB
Raw Blame History

Server work the mobile Calendar & Contacts apps need

From: the mobile side, 2026-08-04. About: platform/docs/mobile-dav-provisioning.md, which we have now built against.

The mobile apps are written and typecheck. Two screens read your JSON door, and a third does the provisioning flow from §2 of your document. This is the list of what is yours, in the order it unblocks us — nothing here is a complaint about the doc, which was accurate everywhere we could check it.


1. POST /api/dav/provision/ios — the one real blocker

Your §3.4 specifies this endpoint and marks it as unwritten. It is the only thing standing between the iOS app and one-tap setup; everything else on our side is done and waiting.

What we already call, exactly as specified:

POST /api/dav/provision/ios
  Authorization: Bearer <the ordinary session JWT>
  { "deviceLabel": "iPhone — Officer app" }

→ 200 { "url": "https://<host>/dav/provision/<token>.mobileconfig", "expiresAt": "<ISO 8601>" }

We open url with Linking.openURL, which is Safari. We do not use a WebView — your §3.6 is right that it silently does nothing.

What we do today without it: provisionIos() turns a 404 or a 501 into a typed DavProvisionUnavailable, and the screen falls back to showing the host, username and a freshly-minted password with copy buttons, plus the §1.6 warning about giving the CardDAV account an explicit collection URL. So the feature degrades rather than breaking — but it is exactly the hand-typing the document exists to eliminate.

Three things we would ask you to hold to, all from your own §3–§5:

  • Content-Type: application/x-apple-aspen-config. Served as application/octet-stream the file downloads and iOS ignores it.
  • Single use, ≤5 minute TTL, deleted on first fetch. It is an unauthenticated URL with a live password in it.
  • One profile containing both the CalDAV and CardDAV payloads, and a stable PayloadIdentifier. That is what makes re-running setup replace the profile instead of stacking a second account, and it is what sidesteps the account-merge trap in §1.6 entirely.

Signing: your §3.5's note about re-signing on certificate renewal is the part most likely to be forgotten six months from now, since nothing fails until someone installs a profile. If the renewal hook is not wired at the same time as the endpoint, it is worth an alarm rather than a comment.

A 501 would help. If the endpoint is going to exist before it works, please answer 501 rather than 404 — we treat both as "fall back", but 501 tells us the server knows about the feature, and it is the difference between "this Officer is too old" and "this Officer is broken" in a support conversation.


2. Confirm the DAV user id, or give us a way to ask

§1.4 says the principal is /dav/<userId>/ and that <userId> is "the numeric account id — 1 on a single-user instance, which every Officer instance is."

We do not hardcode 1. We derive it from the collection paths /api/caldav/_officer/collections returns, and fall back to the authenticated user's id from /auth/me. That works, and it needs nothing from you — but two questions we could not answer from the code:

  1. Is the DAV <userId> always the same integer as the platform user id? They are equal on the reference deployment. If they are the same field by construction, say so in §1.4 and we will simplify; if they could ever diverge, the collections list is the only safe source and §1.4 should warn against the obvious shortcut.
  2. Can a collection ever live outside /dav/<userId>/? The sidecar rejects a collection query parameter that does not start with /dav/<userId>/, so we assume no.

3. Optional, and we are not asking for it yet: writes

The JSON door is read-only, which we think is right, and our screens are read-only to match. Editing happens in the phone's native Calendar and Contacts apps over the DAV account, which already handle recurrence, invitations, reminders and offline edits.

We raise it only so the boundary is deliberate on both sides: if in-app editing is ever wanted, it needs the sidecar to grow a write path. We will not reach around it and PUT iCalendar at /dav from the app — that door authenticates with a DAV app password, and §5.5 says the app must not keep one. If you do build it, the minimum useful surface is create/update/delete for a VEVENT and a VCARD, by collection path and UID, with the sidecar owning the iCalendar generation exactly as it owns the parsing today.


4. Two smaller things

lastUsedAt throttling. §6 says it is written at most once a minute, and we honour that: after provisioning we poll GET /api/dav/passwords every 5 s for 2 minutes before showing anything discouraging. If that throttle ever changes, it changes our timeout — worth a line in §6.

Revocation is in the app, per §5.7, including revoking the previous credential with the same device label when setup is re-run (your §8's suggestion, answered the way you suggested). Nothing needed from you; noted so you know the app can create and destroy these rows.


5. What we built, so you can see the shape

  • packages/core/src/services/caldav.ts — the JSON door client. Reads /_officer/{collections,events,contacts} and nothing else.
  • packages/core/src/services/dav.ts — app passwords, endpoint derivation, the iOS profile call, the DAVx⁵ hand-off.
  • packages/core/src/screens/{calendar,contacts}/ — an agenda and an AZ list, both read-only.
  • packages/core/src/screens/dav/DavSyncScreen.tsx — §2's screen.

Two details from your sidecar we deliberately preserved, because both would be easy to get wrong in a UI: rrule is displayed as a "repeats" badge and never expanded into occurrences, and all-day dates are grouped without being parsed as a Date20260805 through new Date() is midnight UTC, which lands on the previous day for anyone west of it.


Summary

Needed from you Blocks
1 POST /api/dav/provision/ios + token-gated .mobileconfig, signed, with the renewal hook one-tap iOS setup
2 Confirm <userId> semantics in §1.4 nothing — a simplification
3 Sidecar write path nothing — only if in-app editing is ever wanted
4 Tell us if the lastUsedAt throttle changes nothing today

Only row 1 is a blocker. Everything else is us writing down what we assumed, so that if an assumption is wrong it fails in a review rather than on someone's phone.