docs(chat): chat spec using CHANNEL_MESSAGE and existing MessageType

This commit is contained in:
LeCoffre Deployment 2025-10-05 12:03:33 +00:00
parent 4d672bbbae
commit 4262442377

148
docs/chat-spec.md Normal file
View File

@ -0,0 +1,148 @@
### 4NK Chat via iframe (ihm_client) and postMessage
This document specifies a chat feature embedded as an iframe of `ihm_client`, communicating with the host application through `postMessage`. It maximizes reuse of existing `MessageType` where appropriate and introduces a minimal, versioned `CHAT_*` message family for chat-specific flows.
### Scope and roles
- **Host app**: embeds `ihm_client` in an iframe, uses `sdk_client` or `skeleton` `MessageBus` to exchange messages.
- **ihm_client (iframe)**: renders the chat UI, persists data, validates tokens, and routes messages.
- **sdk_common**: unchanged. No modification required for the chat feature.
- **skeleton**: integrates the iframe and routes messages via `iframe.contentWindow.postMessage` with strict `origin`.
### Reused MessageType and rationale
Use the existing `MessageType` values from `ihm_client/src/models/process.model.ts` as-is, where they semantically fit:
- `LISTENING`: iframe readiness ping sent by `ihm_client` at startup. The host treats this as the chat iframe being ready.
- `ERROR`: generic error channel for any failure (validation, auth, unsupported message). Include `code`, `reason`, and `refMessageId` in the payload.
- `VALIDATE_TOKEN` and `RENEW_TOKEN`: reuse token validation/refresh flows for gated chat operations.
- `GET_PAIRING_ID`: allows host to retrieve the user/device pairing id to correlate chat identity if needed.
These are intentionally reused to avoid duplicating platform primitives. All chat-specific operations are namespaced under `CHAT_*` below.
### Message envelope (shared)
- `type`: one of existing `MessageType` or a `CHAT_*` value (see below)
- `messageId`: string (uuid v4), unique per message
- `correlationId`: string (optional), set on replies to refer to the original `messageId`
- `ts`: ISO 8601 timestamp
- `origin`: implicit from `postMessage` context; the receiver must validate `event.origin`
- `payload`: object, schema depends on `type`
- `version`: string, e.g. `chat/1.0`
Validation: implemented locally (no `sdk_common` changes). Add lightweight runtime guards in `ihm_client` and optional helpers in `sdk_client`/`skeleton`.
### Single generic message type for chat
Introduce only one new `MessageType` value: `CHANNEL_MESSAGE`.
All chat operations are multiplexed through `CHANNEL_MESSAGE` using an `action` field inside the payload. This minimizes new types while reusing existing primitives (`LISTENING`, `ERROR`, `VALIDATE_TOKEN`, `RENEW_TOKEN`, `GET_PAIRING_ID`).
`CHANNEL_MESSAGE` payload schema:
- `action`: one of
- `'SUBSCRIBE' | 'UNSUBSCRIBE'`
- `'SEND'` (send a message)
- `'EVENT'` (deliver an event from iframe to host)
- `'TYPING'`
- `'READ'`
- `'HISTORY_REQ' | 'HISTORY_RES'`
- `'CHANNEL_LIST_REQ' | 'CHANNEL_LIST_RES'`
- `channelId?`: string (required for channel-scoped actions)
- `correlationId?`: string (used to pair req/res)
- `data?`: object; action-specific payload:
- `SUBSCRIBE/UNSUBSCRIBE`: `{}`
- `SEND`: `{ content: string, contentType?: 'text/markdown'|'text/plain', attachments?: Attachment[] }`
- `EVENT`: `{ event: 'MESSAGE'|'DELIVERED'|'ERROR'|'PRESENCE', payload: any }`
- `TYPING`: `{ isTyping: boolean }`
- `READ`: `{ messageIds: string[] }`
- `HISTORY_REQ`: `{ beforeTs?: string, limit?: number }`
- `HISTORY_RES`: `{ messages: ChatMessage[], nextBeforeTs?: string }`
- `CHANNEL_LIST_REQ`: `{}`
- `CHANNEL_LIST_RES`: `{ channels: ChatChannel[] }`
Types used above:
- `Attachment`: `{ id?: string, name: string, mime: string, size?: number, dataRef?: string }`
- `ChatMessage`: same structure as `CHAT_MESSAGE.payload`
- `ChatChannel`: `{ channelId: string, title?: string, unread?: number, lastMessageTs?: string }`
### Security and origin validation
- The host must send a `CHAT_HANDSHAKE` immediately upon receiving `LISTENING` from the iframe.
- The iframe must validate `event.origin` against `allowedOrigins` and respond with `CHAT_ACK` or `ERROR`.
- All operations that require authentication should reuse `VALIDATE_TOKEN`/`RENEW_TOKEN` before proceeding; the iframe already has token flows.
- Do not place secrets in clear within `postMessage` payloads; if encryption is needed later, wrap `content` with a ciphertext field (out of scope for v1).
### Persistence and offline (ihm_client)
- Reuse the service worker + IndexedDB layer (`database.service.ts` and `service-workers/database.worker.js`).
- New stores: `channels` and `messages` (indexed by `channelId`, `serverTs`, `messageId`).
- Batch writes can reuse existing `BATCH_WRITING` path; single writes via `ADD_OBJECT`.
- Outbox: queue unsent `CHAT_MESSAGE_SEND` for retry; deliver `CHAT_ACK` only when accepted to the outbox; emit delivery `CHAT_MESSAGE` when persisted/confirmed.
### Integration points (code)
- `ihm_client/src/router.ts`:
- Add handlers in `registerAllListeners()` switch for the new `CHAT_*` types (keep existing `LISTENING`, `ERROR`, token flows).
- Use `window.parent.postMessage` for responses, preserving `messageId`/`correlationId`.
- `skeleton/src/sdk/MessageBus.ts`:
- Reuse existing `postMessage` to iframe; add a registry for pending requests keyed by `messageId` to resolve promises on `correlationId`.
- Filter by strict `origin`.
- `sdk_client`:
- Provide a facade `chat.ts` exporting: `initChat`, `subscribe`, `unsubscribe`, `sendMessage`, `setTyping`, `markRead`, `getHistory`, `listChannels` (helpers and runtime checks local to this module).
- `sdk_common`:
- No changes.
### State flows
1) Ready
- iframe → host: `LISTENING` (déjà existant)
- host may optionally validate tokens using `VALIDATE_TOKEN` / `RENEW_TOKEN`.
2) Send message
- host → iframe: `CHANNEL_MESSAGE { action: 'SEND', channelId, data: { content, ... } }`
- iframe → host: `CHANNEL_MESSAGE { action: 'EVENT', data: { event: 'DELIVERED', payload: { refMessageId } } }`
- iframe → host: `CHANNEL_MESSAGE { action: 'EVENT', data: { event: 'MESSAGE', payload: ChatMessage } }`
3) Typing / Read
- host ↔ iframe: `CHANNEL_MESSAGE { action: 'TYPING', channelId, data: { isTyping } }`
- host → iframe: `CHANNEL_MESSAGE { action: 'READ', channelId, data: { messageIds } }`
4) History / Channels
- host → iframe: `CHANNEL_MESSAGE { action: 'HISTORY_REQ', channelId, data: { beforeTs?, limit? } }`
- iframe → host: `CHANNEL_MESSAGE { action: 'HISTORY_RES', channelId, data: { messages, nextBeforeTs? } }`
- host → iframe: `CHANNEL_MESSAGE { action: 'CHANNEL_LIST_REQ' }`
- iframe → host: `CHANNEL_MESSAGE { action: 'CHANNEL_LIST_RES', data: { channels } }`
### Error handling
- Use existing `ERROR` type across all failures. Payload:
- `{ code: 'UNAUTHORIZED'|'INVALID_PAYLOAD'|'UNSUPPORTED_VERSION'|'ORIGIN_FORBIDDEN'|'INTERNAL', reason: string, details?: any, refMessageId?: string }`
### Versioning
- `version: 'chat/1.0'` in all `CHAT_*` envelopes.
- If version mismatch, iframe must respond with `ERROR { code: 'UNSUPPORTED_VERSION' }`.
### Telemetry (optional)
- Log key events: handshake_ok/fail, subscribe/unsubscribe, send/recv, history_page, ack_timeout.
- Avoid logging payload content; log sizes and identifiers only.
### Minimal host-side example
```ts
// skeleton host pseudo-code
const bus = new MessageBus(iframe, { origin });
bus.on('message', (env) => {/* route CHAT_MESSAGE, ACK, ERROR */});
// Wait for LISTENING from iframe, then you can subscribe/send with CHANNEL_MESSAGE:
bus.send({
type: 'CHANNEL_MESSAGE',
messageId: uuid(),
payload: { action: 'SUBSCRIBE', channelId: 'default' }
});
```
### Minimal iframe-side example
```ts
// inside ihm_client router handlers
if (event.data.type === 'CHANNEL_MESSAGE' && event.data.payload?.action === 'SEND') {
// validate origin + token, persist, then
window.parent.postMessage({ type: 'CHANNEL_MESSAGE', messageId: uuid(), payload: { action: 'EVENT', data: { event: 'DELIVERED', payload: { refMessageId: event.data.messageId } } } }, event.origin);
window.parent.postMessage({ type: 'CHANNEL_MESSAGE', messageId: uuid(), payload: { action: 'EVENT', data: { event: 'MESSAGE', payload: {/* delivered message */} } } }, event.origin);
}
```
### Delivery plan
1) Add types and validators in `sdk_common`.
2) Add `chat.ts` facade in `sdk_client` plus `MessageBus` enhancements in `skeleton`.
3) Implement iframe handlers and basic UI in `ihm_client` (list, thread, composer).
4) Persist via existing service worker paths; add `channels`/`messages` stores.
5) Tests: unit (schemas, router), integration (handshake, send/recv, history), E2E (Playwright, mocked backend).