jmap-chat-client 0.1.2

JMAP Chat HTTP client — auth-agnostic, WebSocket and SSE support
Documentation
# jmap-chat-client — Implementation Plan

Auth-agnostic JMAP Chat HTTP client with WebSocket and SSE support.

## Crate Family Position

```
jmap-types
    ├── jmap-base-client              base HTTP client (auth, session, blob, SSE, WebSocket)
    └── jmap-chat-types
            └── jmap-chat-client  ← this crate (depends on jmap-base-client + jmap-chat-types)
```

## What This Crate Is

An HTTP client library for the JMAP Chat extension. Handles:
- JMAP Core request/response (RFC 8620)
- All JMAP Chat methods (Chat, Message, Space, ReadPosition, ChatContact, etc.)
- Blob upload/download (RFC 8620 §6)
- WebSocket event stream (JMAP Chat WebSocket draft)
- SSE event stream
- Pluggable auth: Bearer, Basic, or none (caller supplies the `Authorization` header)

Known consumers: `kithctl` (CLI), any future JMAP Chat web or mobile client.

## What This Crate Is Not

- Not a server-side crate
- Not coupled to a specific auth system — auth is a pluggable provider
- Not opinionated about connection pooling beyond what `reqwest` provides

## Source Material

The original reference implementation was `~/PROJECT/crate-jmapchat-client/`.
This crate has been extracted and the transport layer (auth, HTTP client, error,
blob upload, base SSE, base WebSocket) has been moved into `jmap-base-client`.
What remains in this crate is the chat-specific surface: method handlers,
chat-extended SSE/WS frame wrappers, and chat-specific session/utility helpers.

| Item | Now lives in | Notes |
|---|---|---|
| `JmapClient` (HTTP core) | `jmap-base-client::client` | Consumed via dep; not re-implemented here |
| Auth providers | `jmap-base-client::auth` | `AuthProvider`, `BearerAuth`, `BasicAuth`, `NoneAuth` |
| `ClientError` | `jmap-base-client::error` | Re-exported from this crate's `lib.rs` |
| JMAP request builder | `jmap-base-client::request` | Consumed via dep |
| Base `Session` | `jmap-base-client::session` | Extended here by `ChatSessionExt` (`src/session.rs`) |
| Base `SseEvent` | `jmap-base-client::sse` | Wrapped by `ChatSseEvent` (`src/sse.rs`) |
| Base `WsFrame` | `jmap-base-client::ws` | Wrapped by `ChatWsFrame` (`src/ws.rs`) |
| Blob upload | `jmap-base-client::blob` | Consumed via dep |
| Method impls | `src/methods/` | Chat, Message, Space, SpaceBan, SpaceInvite, ChatContact, CustomEmoji, Blob, Quota, misc. Typed `&Id`/`&State` per bd:JMAP-6by7.3 |
| Utility fns | `src/utils.rs` | `format_receipt_timestamp`, etc. |

Spec:
- `~/PROJECT/jmap-chat-spec/draft-atwood-jmap-chat-00.md`
- `~/PROJECT/jmap-chat-spec/draft-atwood-jmap-chat-wss-00.md`
- `~/PROJECT/jmap-chat-spec/draft-atwood-jmap-chat-push-00.md`

## Dependencies

See `Cargo.toml`. Summary:

- `jmap-types`, `jmap-chat-types`, `jmap-base-client` — workspace path deps
- `ulid`, `chrono` — used by `utils.rs` for receipt-timestamp formatting
- `serde`, `serde_json` — wire format

All HTTP/TLS/WebSocket dependencies (`reqwest`, `tokio-tungstenite`, `url`,
`thiserror`, `tokio`) are transitive through `jmap-base-client`, not direct deps
of this crate.

## Extension Trait Pattern

Cross-crate inherent impls are not valid Rust (orphan rule: only the crate that defines
a type may add inherent methods to it). Chat methods reach `JmapClient` via an extension
trait — `JmapChatExt` — that hands back a `SessionClient` carrying both the client and
the session:

```rust
use jmap_base_client::{JmapClient, Session};
use jmap_chat_client::JmapChatExt;

let session: Session = client.fetch_session().await?;
let sc = client.with_chat_session(session);
let chats = sc.chat_get(&account_id, &ids).await?;
```

Callers must bring the trait into scope: `use jmap_chat_client::JmapChatExt;`

Rust 1.75 AFIT (async fn in trait, via RPITIT) is used for inherent `SessionClient`
methods — no `async-trait` crate needed.

## Typed-Id refactor (bd:JMAP-6by7.3)

The `SessionClient` method API uses **typed `Id` and `State` parameters** end-to-end:

- Id-shaped parameters: `&Id`, `&[Id]`, `Option<&[Id]>`, `Option<&'a Id>` etc.
- State-shaped parameters: `&State`, `Option<&State>`.
- Id-shaped fields on Input/Patch structs (`MessageCreateInput.chat_id`,
  `ChatPatch.pinned_message_ids`, `SpaceAddMemberInput.id`, etc.) are also typed,
  carrying the typed-Id boundary all the way to the call site.
- Caller-chosen creation reference keys (the `client_id: Option<&'a str>` fields,
  `with_client_id(id: &'a str)` methods) remain `String` / `&str` — they are not
  JMAP Ids but caller-chosen creation references resolved by the server.
- Display strings, descriptions, free-text filters, status text, message bodies,
  invite codes, push device-client IDs, and push URLs remain `&str`.

Consequences:
- Inline empty-Id guards have been removed where the typed parameter makes the
  empty case unrepresentable.
- Empty-state guards remain (defence-in-depth) on every `&State` / `Option<&State>`
  parameter.
- Empty-slice guards remain on `/set` `destroy` ids (typed `&[Id]` does not make
  the slice non-empty).

This was the canonical-template propagation of the typed-Id work landed first
in `jmap-calendars-client` (bd:JMAP-6by7.1) and the canonical
`jmap-mail-client` template (bd:JMAP-6by7.2).

## Key Design Decisions vs. jmapchat-client

1. **Use `jmap-types` wire types directly**`JmapRequest`, `JmapResponse`,
   `Invocation`, `Id`, `UTCDate`, `State` come from `jmap-types`, not re-defined here.

2. **Use `jmap-chat-types` domain types directly**`Chat`, `Message`, `Space`, etc.
   come from `jmap-chat-types`. A thin `src/types.rs` remains only for client-side
   auxiliary types that are not wire-format (e.g. `ContactPresenceFilter`).

3. **Auth, transport, session, SSE, WebSocket, blob live in `jmap-base-client`**   `jmap-base-client` is a workspace path dependency and supplies `AuthProvider`,
   `JmapClient`, `ClientError`, the base `SseEvent`/`WsFrame` types, blob upload,
   and session fetch. This crate's `sse.rs` and `ws.rs` are chat-specific *extension*
   layers that wrap the base types with chat semantics (typing, presence) — not
   duplicates of base-client modules.

4. **Auth is unchanged** — the pluggable `AuthProvider` trait and the three built-in
   providers (`BearerAuth`, `BasicAuth`, `NoneAuth`) come from `jmap-base-client`.

## Module Layout

```
src/
  lib.rs        re-exports + JmapChatExt trait
  session.rs    ChatSessionExt — ChatCapability, ChatPushCapability accessors
  sse.rs        ChatSseEvent, ChatSseFrame — chat-specific SSE wrappers over base SseEvent
  ws.rs         ChatWsExt, ChatWsFrame — chat-specific WS wrappers over base WsFrame
  types.rs      Client-side auxiliary types (e.g. ContactPresenceFilter)
  utils.rs      format_receipt_timestamp, etc.
  methods/
    mod.rs          method input/output types, re-exports, SessionClient
    chat.rs         Chat/get, Chat/set, Chat/query, Chat/changes
    message.rs      Message/get, Message/set, Message/query, Message/changes
    space.rs        Space/get, Space/set, Space/query, Space/changes
    space_ban.rs    SpaceBan/* sub-operations
    space_invite.rs SpaceInvite/* sub-operations
    contact.rs      ChatContact/get, ChatContact/set
    custom_emoji.rs CustomEmoji/* methods
    blob.rs         Blob/copy, Blob/lookup
    quota.rs        Quota/get
    misc.rs         Core/echo, PushSubscription/set
```

## Extras-preservation policy (JMAP-lbdy)

Every public method-response struct (Deserialize) defined in this crate
that appears on the JMAP wire carries an `extra` field per the workspace
extras-preservation policy (see workspace `AGENTS.md`):

```rust
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
```

This preserves vendor / site / private-extension fields across
deserialize/serialize round-trip. Wire format is byte-identical when extras
are empty. This crate has no Serialize-only method-argument structs on its
public surface; the policy here applies uniformly to the Deserialize
method-response structs enumerated below.

This crate implements the JMAP Chat draft
(`draft-atwood-jmap-chat-*`) plus Quota (RFC 9425) and the
blob-extension types (`draft-ietf-jmap-blobext-*`).

In scope in this crate (each has at least one round-trip preservation
test named `*_preserves_vendor_extras` in the defining module):

- Method-response structs (Deserialize):
  - `ChatCapability`, `ChatPushCapability` in `src/session.rs`
  - `Quota` in `src/methods/quota.rs`
  - `BlobLookupEntry`, `BlobLookupResponse`, `BlobObject`,
    `BlobConvertResponse` in `src/methods/blob.rs`
  - `PushSubscriptionCreateResponse`, `TypingResponse`,
    `SpaceJoinResponse` in `src/methods/mod.rs`

The crate also re-exports standard response wrappers (`GetResponse<T>`,
`SetResponse<T>`, `ChangesResponse`, `QueryResponse`,
`QueryChangesResponse`) from `jmap-types`; those carry their own `extra`
field per JMAP-lbdy.1 and are not re-documented here.

Out of scope (explicitly excluded by the workspace policy):

- Filter / comparator algebra types and control enums — see workspace
  AGENTS.md "Filter algebra and control enums are explicitly EXCLUDED"
  for the full rationale.
- Internal Rust state types (`SessionClient`, the private SSE helpers
  `TypingPayload` / `PresencePayload`) — not wire-format.

### New-type rule

Any new public method-response struct added to this crate that appears on
the JMAP wire MUST include the `extra` field from day one with the
documented serde attributes and at least one round-trip preservation test.
Per the canonical-template propagation rule (workspace AGENTS.md), changes
to the canonical `jmap-mail-client` surface propagate to this crate and to
the other five sibling extension-client crates in lock-step.

## Test Strategy

- Unit tests against a `wiremock` mock server (already used in `jmapchat-client/`)
- Round-trip tests for request serialization using `JmapRequestBuilder`
- WebSocket tests using `tokio-tungstenite`'s test helpers
- No live network in CI — all tests use local mocks or recorded fixtures