@chitchat/sdk-web 0.1.0-dev.1 → 0.2.0-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,136 @@
1
1
  # ChitChat Web SDK
2
2
 
3
- `createChitChatWebClient` always uses WSS. It authenticates through the first binary Protobuf frame, never by placing a session token in a URL. Supply the generated Protobuf codec and a function that fetches a short-lived project-user session token. Plain `ws://` is rejected except for an explicit `allowInsecureWs: true` localhost-only development configuration.
3
+ `@chitchat/sdk-web` connects a browser to ChitChat realtime using secure WebSocket (**WSS**) and binary Protobuf frames. Browsers cannot use raw TCP, so this package never attempts it.
4
4
 
5
- Raw TCP is not available in browsers and is never attempted by this module.
5
+ This package is currently a **development prerelease**. Pin the version you have tested before shipping an application.
6
+
7
+ ## SDK foundation in v0.2
8
+
9
+ The Web SDK now rejects text or JSON transport frames. `protobuf.encode` and optional `protobuf.encodeAuth` must return binary Protobuf bytes. Connection opening has a bounded timeout and transport failures are isolated from stale sockets.
10
+
11
+ ## What you need first
12
+
13
+ 1. A backend session endpoint created with `@chitchat/sdk-server`. It authenticates the person in your product and returns a short-lived `{ token, expiresAt }` response.
14
+ 2. An HTTPS realtime endpoint, for example `https://realtime.example.com`. The SDK converts it to `wss://realtime.example.com`.
15
+ 3. A generated Protobuf codec adapter that is compatible with the ChitChat gateway’s current `chitchat.protobuf.v1` envelope and authentication frame.
16
+ 4. A durable browser outbox implementation (IndexedDB) if messages must survive page reloads.
17
+
18
+ ```bash
19
+ npm install @chitchat/sdk-core@development @chitchat/sdk-web@development
20
+ ```
21
+
22
+ Do not install `@chitchat/sdk-server` in a browser application and never expose a ChitChat server API key in JavaScript, public environment variables, source maps, or logs.
23
+
24
+ ## Minimal connection setup
25
+
26
+ ```js
27
+ import { createBackendTokenProvider } from '@chitchat/sdk-core';
28
+ import { createChitChatWebClient } from '@chitchat/sdk-web';
29
+ import { chitchatEnvelopeCodec } from './generated/chitchat-envelope-codec.js';
30
+
31
+ const tokenProvider = createBackendTokenProvider({
32
+ endpoint: 'https://api.example.com/api/chitchat/session',
33
+ credentials: 'include',
34
+ getBody: () => ({ deviceId: getStableInstallationId() })
35
+ });
36
+
37
+ const client = createChitChatWebClient({
38
+ endpoint: 'https://realtime.example.com',
39
+ tokenProvider,
40
+ // This must encode/decode the deployed Protobuf envelope—not JSON.
41
+ protobuf: chitchatEnvelopeCodec,
42
+ storage: indexedDbOutbox
43
+ });
44
+
45
+ client.on('connected', () => setRealtimeState('connected'));
46
+ client.on('disconnected', () => setRealtimeState('reconnecting'));
47
+ client.on('message', (frame) => handleChitChatFrame(frame));
48
+ client.on('error', (error) => reportNonSensitiveRealtimeError(error));
49
+
50
+ await client.connect();
51
+ ```
52
+
53
+ When the user signs out or switches accounts, close the old client before creating a new one:
54
+
55
+ ```js
56
+ await client.close();
57
+ tokenProvider.invalidate();
58
+ ```
59
+
60
+ ## Protobuf codec requirement
61
+
62
+ Pass an object with these two functions:
63
+
64
+ ```js
65
+ const chitchatEnvelopeCodec = {
66
+ encode(frame) {
67
+ // Convert the SDK frame to the exact generated Protobuf envelope bytes.
68
+ // The first auth frame must match the gateway handshake schema.
69
+ return generatedEnvelope.encode(frame).finish();
70
+ },
71
+ decode(binary) {
72
+ // Convert ArrayBuffer / binary data from the gateway into an SDK frame.
73
+ return generatedEnvelope.decode(new Uint8Array(binary));
74
+ }
75
+ };
76
+ ```
77
+
78
+ If the gateway has a distinct authentication envelope, add `encodeAuth({ token, protocol })` to the codec. It must also return binary bytes.
79
+
80
+ Do not use a JSON mock codec in production. This SDK release exposes the adapter boundary; the generated ChitChat envelope codec and the production handshake compatibility test must come from the deployed platform protocol. If that codec is unavailable, do not enable realtime for users yet.
81
+
82
+ ## Sending and receiving
83
+
84
+ ```js
85
+ const localMessageId = await client.send('chat.message', {
86
+ conversationId: 'conversation_123',
87
+ body: 'Hello'
88
+ });
89
+
90
+ client.on('ack', (messageId) => {
91
+ // The gateway acknowledged a queued outgoing record.
92
+ markMessageTransportAcknowledged(messageId);
93
+ });
94
+ ```
95
+
96
+ `send()` queues the outgoing record; it is not a peer-delivered or read receipt. Map the platform’s message/receipt frames to your UI separately.
97
+
98
+ ## WSS and local development
99
+
100
+ - `https://...` endpoints are converted to `wss://...`.
101
+ - `wss://...` endpoints are accepted directly.
102
+ - `http://...` and `ws://...` are rejected by default.
103
+ - For a loopback-only local test, opt in explicitly:
104
+
105
+ ```js
106
+ const client = createChitChatWebClient({
107
+ endpoint: 'http://127.0.0.1:5222',
108
+ tokenProvider,
109
+ protobuf: chitchatEnvelopeCodec,
110
+ allowInsecureWs: true
111
+ });
112
+ ```
113
+
114
+ This local exception works only for `localhost`, `127.0.0.1`, or `::1`; never enable it for LAN, staging, or production addresses.
115
+
116
+ ## Common errors
117
+
118
+ | Error | Fix |
119
+ | --- | --- |
120
+ | `Web realtime requires WSS` | Use an HTTPS/WSS realtime endpoint. Do not use TCP or non-local WS from a browser. |
121
+ | `WSS connection failed` | Check DNS, TLS certificate/hostname, gateway availability, session token, and gateway origin policy. |
122
+ | Connection opens then protocol errors | Your codec or first Protobuf auth frame does not match the deployed gateway schema. Regenerate/use the correct protocol adapter. |
123
+ | Messages disappear after reload | Supply an IndexedDB-backed `storage`; the default core store is memory-only. |
124
+ | Browser session endpoint is rejected | Verify CORS/cookie policy and authenticate the user in your own backend before issuing a ChitChat session. |
125
+
126
+ ## Security checklist
127
+
128
+ - Keep the server API key in your backend only.
129
+ - Use HTTPS for your token endpoint and WSS for realtime.
130
+ - Do not put session tokens in URLs, logs, analytics, or error reports.
131
+ - Restrict the backend CORS policy to your actual web origins and protect the session endpoint against CSRF when using cookies.
132
+ - Call `close()` and invalidate the token provider at logout.
133
+
134
+ ## Current scope
135
+
136
+ The Web SDK handles a WSS transport and the reliable client lifecycle. Chat UI, conversation APIs, attachments, read receipts, WebRTC media, meetings, streaming, notifications, and agents require their own deployed platform APIs and protocol handlers.
package/index.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import type { ReliableRealtimeClient, ReliableRealtimeClientOptions, RealtimeFrame } from '@chitchat/sdk-core';
2
+ export interface ProtobufCodec { encode(frame: RealtimeFrame): ArrayBuffer | ArrayBufferView; decode(binary: ArrayBuffer | Blob): RealtimeFrame; encodeAuth?(input: { token: string; protocol: 'chitchat.protobuf.v1' }): ArrayBuffer | ArrayBufferView; openTimeoutMs?: number; }
3
+ export function toWssEndpoint(endpoint: string, options?: { allowInsecureWs?: boolean }): string;
4
+ export function createWssTransportFactory(options: ProtobufCodec & { WebSocketImpl?: typeof WebSocket; allowInsecureWs?: boolean }): ReliableRealtimeClientOptions['webTransportFactory'];
5
+ export function createChitChatWebClient(options: Omit<ReliableRealtimeClientOptions, 'runtime' | 'webTransportFactory'> & { protobuf: ProtobufCodec; allowInsecureWs?: boolean }): ReliableRealtimeClient;
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@chitchat/sdk-web",
3
- "version": "0.1.0-dev.1",
3
+ "version": "0.2.0-dev.0",
4
4
  "main": "src/index.js",
5
- "exports": "./src/index.js",
6
- "files": ["src", "README.md", "LICENSE"],
5
+ "exports": { ".": { "types": "./index.d.ts", "require": "./src/index.js" } },
6
+ "types": "index.d.ts",
7
+ "files": ["src", "index.d.ts", "README.md", "LICENSE"],
7
8
  "engines": { "node": ">=18" },
8
- "dependencies": { "@chitchat/sdk-core": "0.1.0-dev.1" },
9
+ "dependencies": { "@chitchat/sdk-core": "0.2.0-dev.0" },
9
10
  "license": "UNLICENSED",
10
11
  "publishConfig": { "access": "public", "tag": "development" }
11
12
  }
package/src/index.js CHANGED
@@ -14,27 +14,62 @@ const toWssEndpoint = (endpoint, { allowInsecureWs = false } = {}) => {
14
14
  return url.toString();
15
15
  };
16
16
 
17
- const createWssTransportFactory = ({ WebSocketImpl = globalThis.WebSocket, encode, decode, allowInsecureWs = false }) => {
17
+ const isBinary = (value) => value instanceof ArrayBuffer || ArrayBuffer.isView(value) || (typeof Buffer !== 'undefined' && Buffer.isBuffer(value));
18
+ const binaryFrame = (value, name) => {
19
+ if (!isBinary(value)) throw new Error(`${name} must return a binary Protobuf frame`);
20
+ return value;
21
+ };
22
+
23
+ const createWssTransportFactory = ({ WebSocketImpl = globalThis.WebSocket, encode, decode, encodeAuth, allowInsecureWs = false, openTimeoutMs = 15_000 }) => {
18
24
  if (!WebSocketImpl || typeof encode !== 'function' || typeof decode !== 'function') throw new Error('WebSocket implementation and protobuf encode/decode functions are required');
25
+ if (typeof openTimeoutMs !== 'number' || !Number.isFinite(openTimeoutMs) || openTimeoutMs < 100 || openTimeoutMs > 120_000) throw new Error('openTimeoutMs must be between 100 and 120000');
19
26
  return async ({ endpoint, token, protocol }) => {
20
- let socket; let onMessage = () => {}; let onClose = () => {};
27
+ let socket; let onMessage = () => {}; let onClose = () => {}; let opened = false;
28
+ const encodeFrame = (frame) => binaryFrame(encode(frame), 'protobuf.encode');
29
+ const encodeAuthentication = () => binaryFrame(encodeAuth ? encodeAuth({ token, protocol }) : encode({ type: 'auth', token, protocol }), 'protobuf authentication encoder');
21
30
  return {
22
31
  onMessage: (listener) => { onMessage = listener; },
23
32
  onClose: (listener) => { onClose = listener; },
24
33
  open: () => new Promise((resolve, reject) => {
25
- socket = new WebSocketImpl(toWssEndpoint(endpoint, { allowInsecureWs }), protocol);
26
- socket.binaryType = 'arraybuffer';
27
- socket.onopen = () => { socket.send(encode({ type: 'auth', token })); resolve(); };
28
- socket.onerror = () => reject(new Error('WSS connection failed'));
29
- socket.onclose = (event) => onClose({ code: event.code, reason: event.reason });
30
- socket.onmessage = (event) => onMessage(decode(event.data));
34
+ let timer;
35
+ let settled = false;
36
+ const finish = (handler, value) => { if (settled) return; settled = true; clearTimeout(timer); handler(value); };
37
+ try {
38
+ socket = new WebSocketImpl(toWssEndpoint(endpoint, { allowInsecureWs }), protocol);
39
+ socket.binaryType = 'arraybuffer';
40
+ timer = setTimeout(() => { try { socket.close(); } catch (_) {} finish(reject, new Error('WSS connection timed out')); }, openTimeoutMs);
41
+ socket.onopen = () => {
42
+ try { socket.send(encodeAuthentication()); opened = true; finish(resolve); }
43
+ catch (error) { finish(reject, error); try { socket.close(); } catch (_) {} }
44
+ };
45
+ socket.onerror = () => finish(reject, new Error('WSS connection failed'));
46
+ socket.onclose = (event = {}) => {
47
+ if (!opened) finish(reject, new Error(event.reason || 'WSS connection closed before opening'));
48
+ onClose({ code: event.code, reason: event.reason });
49
+ };
50
+ socket.onmessage = (event) => {
51
+ try { onMessage(decode(event.data)); }
52
+ catch (_) { try { socket.close(); } catch (_) {} onClose({ code: 'PROTOCOL_DECODE_ERROR', reason: 'Invalid Protobuf frame' }); }
53
+ };
54
+ } catch (error) { finish(reject, error); }
31
55
  }),
32
- send: (frame) => socket.send(encode(frame)),
33
- close: () => new Promise((resolve) => { if (!socket || socket.readyState === 3) return resolve(); socket.onclose = () => resolve(); socket.close(); })
56
+ send: (frame) => {
57
+ if (!socket || socket.readyState !== 1) return Promise.reject(new Error('WSS transport is not open'));
58
+ try { socket.send(encodeFrame(frame)); return Promise.resolve(); } catch (error) { return Promise.reject(error); }
59
+ },
60
+ close: () => new Promise((resolve) => {
61
+ if (!socket || socket.readyState === 3) return resolve();
62
+ const current = socket.onclose;
63
+ socket.onclose = (event) => { if (typeof current === 'function') current(event); resolve(); };
64
+ try { socket.close(); } catch (_) { resolve(); }
65
+ })
34
66
  };
35
67
  };
36
68
  };
37
69
 
38
- const createChitChatWebClient = ({ endpoint, tokenProvider, protobuf, allowInsecureWs = false, ...options }) => new ReliableRealtimeClient({ endpoint, tokenProvider, runtime: 'web', webTransportFactory: createWssTransportFactory({ ...protobuf, allowInsecureWs }), ...options });
70
+ const createChitChatWebClient = ({ endpoint, tokenProvider, protobuf, allowInsecureWs = false, ...options }) => {
71
+ if (!protobuf || typeof protobuf !== 'object') throw new Error('protobuf codec is required');
72
+ return new ReliableRealtimeClient({ endpoint, tokenProvider, runtime: 'web', webTransportFactory: createWssTransportFactory({ ...protobuf, allowInsecureWs }), ...options });
73
+ };
39
74
 
40
75
  module.exports = { createChitChatWebClient, createWssTransportFactory, toWssEndpoint };