docs(chat): chat spec using CHANNEL_MESSAGE and existing MessageType
This commit is contained in:
parent
4d672bbbae
commit
4262442377
148
docs/chat-spec.md
Normal file
148
docs/chat-spec.md
Normal 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).
|
||||
Loading…
x
Reference in New Issue
Block a user