mobile-api-keys: record what the client actually does now
The client half exists in monorepo-mobile as of f29774a, with OffChat as the proof of concept, so this document is no longer purely forward-looking. Adds a section covering what shipped, the two places the advice here was wrong about the client — one key per app is not possible on mobile (shared-session puts one credential in front of all nineteen apps), and signout was never the problem, distress signout was — and two decisions worth a second opinion: clearing the credential on 401 only, and leaving the traded-in JWT to expire rather than blacklisting it, because that handler clears vault tokens keyed on the user.
This commit is contained in:
@@ -6,6 +6,15 @@ planned-but-missing unless it says so.
|
|||||||
**The mobile apps are not built here.** This document is the boundary: what the server now accepts, what
|
**The mobile apps are not built here.** This document is the boundary: what the server now accepts, what
|
||||||
changes in `monorepo-mobile`, and what does not.
|
changes in `monorepo-mobile`, and what does not.
|
||||||
|
|
||||||
|
> **Update, later on 2026-08-08 — the client side now exists, in `monorepo-mobile`.** `@officer/core`
|
||||||
|
> gained `services/api-keys.ts` and `useAuth` learned that a key is a session; **OffChat (`apps/chat`) is
|
||||||
|
> the proof of concept** and is the only app wired up. The other eighteen are untouched and behave
|
||||||
|
> exactly as before.
|
||||||
|
>
|
||||||
|
> **None of it has been compiled or run** — it was written on the Linux box, which has no `node_modules`
|
||||||
|
> for that repo and no Mac. Read §"What the client actually does now" for what shipped, what it changed
|
||||||
|
> about the advice below, and the two things this document got wrong about the client.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## The short version
|
## The short version
|
||||||
@@ -256,6 +265,61 @@ document was written, along with a WebSocket upgrade on `/api/cliamp/ws?token=<k
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## What the client actually does now
|
||||||
|
|
||||||
|
Built in `monorepo-mobile` on 2026-08-08, against the server described above. **Written, never compiled**
|
||||||
|
— no `node_modules` in that tree on this machine and no Mac, so not even `tsc` has seen it.
|
||||||
|
|
||||||
|
### New in `@officer/core`
|
||||||
|
|
||||||
|
- **`src/services/api-keys.ts`** — `listApiKeys` / `createApiKey` / `revokeApiKey`, `isApiKey`,
|
||||||
|
`defaultApiKeyName`, and `revokeApiKeyByCredential` (below). Shaped to mirror `dav.ts`'s app
|
||||||
|
passwords, which is the same mint-once-hash-forever contract.
|
||||||
|
- **`src/services/device-name.ts`** — `deviceName()`, lifted out of `dav.ts`'s `deviceLabel()` so both
|
||||||
|
credential features name a device the same way. `deviceLabel()` now composes it; no caller changed.
|
||||||
|
- **`useAuth`** — `signin(email, password, { deviceKeyName })` opts into path (a); `signInWithApiKey(key)`
|
||||||
|
is path (b).
|
||||||
|
|
||||||
|
### Where this document was wrong about the client
|
||||||
|
|
||||||
|
**1. There is no such thing as "one key per app" on mobile.** §(a) offers per-device or per-app as a free
|
||||||
|
choice. It is not: `packages/core/src/services/shared-session.ts` holds **one token per server, shared
|
||||||
|
across the whole suite** through an iOS keychain access group and an Android signature-permission
|
||||||
|
ContentProvider. Whichever app writes last wins, for all of them. So a key named after an app actively
|
||||||
|
misleads the owner about what revoking it disconnects — the answer is always "the device". The default
|
||||||
|
name is `"<device> — <app>"`, where the app half records only who did the minting.
|
||||||
|
|
||||||
|
**2. "Do not call signout with a key" was not the whole story — `distressSignout` was the real problem.**
|
||||||
|
Under duress the app clears the credential locally _first_, then best-effort revokes server-side via
|
||||||
|
`POST /api/auth/revoke`. That endpoint blacklists a JWT and does nothing whatever for a key, so the
|
||||||
|
moment a phone starts holding keys, distress sign-out silently stops killing the credential. **And it
|
||||||
|
matters more here than it ever did for a JWT: an unrevoked session token still expires in thirty days, an
|
||||||
|
unrevoked key never does.**
|
||||||
|
|
||||||
|
`revokeApiKeyByCredential(baseUrl, key)` is the fix — raw `fetch` (the credential is already out of
|
||||||
|
storage by then, so it cannot go through `request()`), `GET /api/api-keys`, match on the `prefix` the
|
||||||
|
server kept in the clear, `DELETE /api/api-keys/:id`.
|
||||||
|
|
||||||
|
### Two client-side decisions worth a second opinion
|
||||||
|
|
||||||
|
- **A 401 is now the only thing that clears the credential.** `useAuth`'s `/api/auth/me` query used to
|
||||||
|
clear on _any_ throw. A network error is `ApiError(0)` and a timeout is `ApiError(408)`, so launching
|
||||||
|
with no connectivity destroyed a working session and dropped the user at a login screen that needs the
|
||||||
|
network to be useful — and a 403 produced the logout loop this document warns about. Pre-existing, not
|
||||||
|
caused by keys, fixed while in there.
|
||||||
|
- **The traded-in JWT is left to expire, not blacklisted.** Minting a key and keeping it drops a live
|
||||||
|
thirty-day token on the floor, and `POST /api/auth/signout` is the obvious tidy-up — but that handler
|
||||||
|
also calls `clearVaultTokens(user.id)`, keyed on the **user**, not the session. Blacklisting the
|
||||||
|
discarded token would therefore drop a vault session brokered on another of the owner's devices, as a
|
||||||
|
side effect of signing in. Not worth it for a token nothing holds and nothing will send again.
|
||||||
|
|
||||||
|
### Still not built on the client
|
||||||
|
|
||||||
|
No key-management screen in any app — listing and revoking remain web-only, per "Not built" above. The
|
||||||
|
service verbs exist (`listApiKeys`, `revokeApiKey`) if that changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Where this lives on the server
|
## Where this lives on the server
|
||||||
|
|
||||||
- `src/servers/auth-token.ts` — the key format and `resolveAuthToken`, the single function that turns a
|
- `src/servers/auth-token.ts` — the key format and `resolveAuthToken`, the single function that turns a
|
||||||
|
|||||||
Reference in New Issue
Block a user