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>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
# 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 A–Z 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 `Date` — `20260805` 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.
|
||||
Reference in New Issue
Block a user