@chitchat/sdk-web 0.2.0-dev.3 → 0.3.0-dev.1

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,12 @@
1
1
  # ChitChat Web SDK
2
2
 
3
+ ## 0.3.0-dev.0 source release
4
+
5
+ Adds createChitChatWebCalls and @chitchat/sdk-web/react (useCallState, CallVideo). Browser signalling remains binary protobuf over WSS; media uses WebRTC.
6
+
7
+ This version is **not yet published or production-certified**. See the [calling quickstart](../docs/CALLING.md), [all-five SDK audit](../docs/GAP_AUDIT.md), [migration/support targets](../docs/RELEASE.md), and [local tarball installation](../README.md). Existing npm development tags may still resolve to older SDKs.
8
+
9
+
3
10
  `@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
11
 
5
12
  This package is currently a **development prerelease**. Pin the version you have tested before shipping an application.
@@ -65,7 +72,10 @@ Use `createRealtimeEnvelopeCodec({ deviceId, platform })`; it produces the hands
65
72
  ```js
66
73
  const localMessageId = await client.send('chat.message', {
67
74
  conversationId: 'conversation_123',
68
- body: 'Hello'
75
+ receiverId: recipientGatewayId,
76
+ ciphertext: encryptedPayload.ciphertext,
77
+ iv: encryptedPayload.iv,
78
+ authTag: encryptedPayload.authTag
69
79
  });
70
80
 
71
81
  client.on('ack', (messageId) => {
@@ -114,4 +124,4 @@ This local exception works only for `localhost`, `127.0.0.1`, or `::1`; never en
114
124
 
115
125
  ## Current scope
116
126
 
117
- 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.
127
+ The 0.3 source release includes the calling additions described above. See the [gap audit](../docs/GAP_AUDIT.md) for supported behavior and remaining service requirements. Calling is a foreground small-mesh implementation. Server-authoritative rooms, roles, moderation, recording, signed call webhooks, SFU meetings, background native calling and device/browser production certification remain unimplemented or unverified.
package/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
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; isAuthenticationAccepted?(frame: RealtimeFrame): boolean; authenticationError?(frame: RealtimeFrame): string | null; openTimeoutMs?: number; }
2
+ export interface ProtobufCodec { encode(frame: RealtimeFrame): ArrayBuffer | ArrayBufferView; decode(binary: ArrayBuffer | ArrayBufferView): RealtimeFrame; encodeAuth?(input: { token: string; protocol: 'chitchat.protobuf.v1' }): ArrayBuffer | ArrayBufferView; isAuthenticationAccepted(frame: RealtimeFrame): boolean; authenticationError?(frame: RealtimeFrame): string | null; openTimeoutMs?: number; maxBufferedAmount?: number; }
3
3
  export function toWssEndpoint(endpoint: string, options?: { allowInsecureWs?: boolean }): string;
4
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;
5
+ export function createChitChatWebClient(options: Omit<ReliableRealtimeClientOptions, 'runtime' | 'webTransportFactory'> & { protobuf: ProtobufCodec; WebSocketImpl?: typeof WebSocket; allowInsecureWs?: boolean }): ReliableRealtimeClient;
6
+
7
+ export function createChitChatWebCalls(options: Omit<import('@chitchat/sdk-core').CallsOptions, 'rtc'> & { rtc?: import('@chitchat/sdk-core').WebRtcRuntime }): import('@chitchat/sdk-core').CallsClient;
package/package.json CHANGED
@@ -1,18 +1,61 @@
1
1
  {
2
2
  "name": "@chitchat/sdk-web",
3
- "version": "0.2.0-dev.3",
3
+ "version": "0.3.0-dev.1",
4
4
  "description": "Browser WSS transport for the ChitChat Developer Platform",
5
- "repository": { "type": "git", "url": "git+https://github.com/07Akashh/E2E-Chat.git", "directory": "developer-web/sdk/chitchat-web" },
6
- "bugs": { "url": "https://github.com/07Akashh/E2E-Chat/issues" },
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/07Akashh/E2E-Chat.git",
8
+ "directory": "developer-web/sdk/chitchat-web"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/07Akashh/E2E-Chat/issues"
12
+ },
7
13
  "homepage": "https://github.com/07Akashh/E2E-Chat/tree/main/docs/developer-platform",
8
14
  "main": "src/index.js",
9
- "exports": { ".": { "types": "./index.d.ts", "require": "./src/index.js", "import": "./src/index.js" } },
15
+ "exports": {
16
+ ".": {
17
+ "types": "./index.d.ts",
18
+ "require": "./src/index.js",
19
+ "import": "./src/index.js"
20
+ },
21
+ "./react": {
22
+ "types": "./react.d.ts",
23
+ "require": "./src/react.js",
24
+ "import": "./src/react.js"
25
+ }
26
+ },
10
27
  "types": "index.d.ts",
11
- "files": ["src", "index.d.ts", "README.md"],
12
- "engines": { "node": ">=18" },
13
- "dependencies": { "@chitchat/sdk-core": "0.2.0-dev.2" },
14
- "keywords": ["chitchat", "realtime", "websocket", "wss", "protobuf"],
28
+ "files": [
29
+ "src",
30
+ "index.d.ts",
31
+ "README.md",
32
+ "react.d.ts"
33
+ ],
34
+ "engines": {
35
+ "node": ">=18"
36
+ },
37
+ "dependencies": {
38
+ "@chitchat/sdk-core": "0.3.0-dev.1"
39
+ },
40
+ "keywords": [
41
+ "chitchat",
42
+ "realtime",
43
+ "websocket",
44
+ "wss",
45
+ "protobuf"
46
+ ],
15
47
  "sideEffects": false,
16
48
  "license": "UNLICENSED",
17
- "publishConfig": { "access": "public", "tag": "development" }
49
+ "publishConfig": {
50
+ "access": "public",
51
+ "tag": "development"
52
+ },
53
+ "peerDependencies": {
54
+ "react": ">=18 <20"
55
+ },
56
+ "peerDependenciesMeta": {
57
+ "react": {
58
+ "optional": true
59
+ }
60
+ }
18
61
  }
package/react.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import type { CallsClient, CallSnapshot } from '@chitchat/sdk-core';
2
+ import type { ForwardRefExoticComponent, RefAttributes, VideoHTMLAttributes } from 'react';
3
+ export function useCallState(client: CallsClient): CallSnapshot;
4
+ export const CallVideo: ForwardRefExoticComponent<VideoHTMLAttributes<HTMLVideoElement> & RefAttributes<HTMLVideoElement> & { stream: MediaStream | null; onPlaybackError?(event: { code: 'PLAYBACK_BLOCKED' }): void }>;
package/src/index.js CHANGED
@@ -1,13 +1,18 @@
1
1
  'use strict';
2
2
 
3
3
  let ReliableRealtimeClient;
4
- try { ({ ReliableRealtimeClient } = require('@chitchat/sdk-core')); } catch (_) { ({ ReliableRealtimeClient } = require('../../chitchat-core/src')); }
4
+ try {
5
+ ({ ReliableRealtimeClient } = require('@chitchat/sdk-core'));
6
+ } catch (_) {
7
+ // Monorepo dev fallback — dynamic so Metro never statically bundles the path.
8
+ ({ ReliableRealtimeClient } = require(['..', '..', 'chitchat-core', 'src'].join('/')));
9
+ }
5
10
 
6
- const isLocalHttpUrl = (url) => ['localhost', '127.0.0.1', '::1'].includes(url.hostname);
11
+ const isLocalHttpUrl = (url) => ['localhost', '127.0.0.1', '::1', '[::1]'].includes(url.hostname);
7
12
  const toWssEndpoint = (endpoint, { allowInsecureWs = false } = {}) => {
8
13
  let url;
9
14
  try { url = new URL(endpoint); } catch (_) { throw new Error('A valid HTTPS or WSS endpoint is required'); }
10
- if (url.username || url.password) throw new Error('Realtime endpoint must not include credentials');
15
+ if (url.username || url.password || url.search || url.hash) throw new Error('Realtime endpoint must not include credentials');
11
16
  if (url.protocol === 'https:') url.protocol = 'wss:';
12
17
  if (url.protocol === 'http:' && allowInsecureWs === true && isLocalHttpUrl(url)) url.protocol = 'ws:';
13
18
  if (url.protocol !== 'wss:' && !(url.protocol === 'ws:' && allowInsecureWs === true && isLocalHttpUrl(url))) throw new Error('Web realtime requires WSS (WS is allowed only for explicit localhost development)');
@@ -20,64 +25,77 @@ const binaryFrame = (value, name) => {
20
25
  return value;
21
26
  };
22
27
 
23
- const createWssTransportFactory = ({ WebSocketImpl = globalThis.WebSocket, encode, decode, encodeAuth, isAuthenticationAccepted, authenticationError, allowInsecureWs = false, openTimeoutMs = 15_000 }) => {
28
+ const createWssTransportFactory = ({ WebSocketImpl = globalThis.WebSocket, encode, decode, encodeAuth, isAuthenticationAccepted, authenticationError, allowInsecureWs = false, openTimeoutMs = 15_000, maxBufferedAmount = 1024 * 1024 }) => {
24
29
  if (!WebSocketImpl || typeof encode !== 'function' || typeof decode !== 'function') throw new Error('WebSocket implementation and protobuf encode/decode functions are required');
25
30
  if (typeof isAuthenticationAccepted !== 'function') throw new Error('isAuthenticationAccepted is required so the SDK can verify the realtime handshake');
26
- if (typeof openTimeoutMs !== 'number' || !Number.isFinite(openTimeoutMs) || openTimeoutMs < 100 || openTimeoutMs > 120_000) throw new Error('openTimeoutMs must be between 100 and 120000');
31
+ if (!Number.isInteger(openTimeoutMs) || openTimeoutMs < 100 || openTimeoutMs > 120_000) throw new Error('openTimeoutMs must be between 100 and 120000');
32
+ if (!Number.isInteger(maxBufferedAmount) || maxBufferedAmount < 1 || maxBufferedAmount > 16 * 1024 * 1024) throw new Error('maxBufferedAmount is invalid');
27
33
  return async ({ endpoint, token, protocol }) => {
28
- let socket; let onMessage = () => {}; let onClose = () => {}; let opened = false; let authenticationPending = true;
29
- const encodeFrame = (frame) => binaryFrame(encode(frame), 'protobuf.encode');
30
- const encodeAuthentication = () => binaryFrame(encodeAuth ? encodeAuth({ token, protocol }) : encode({ type: 'auth', token, protocol }), 'protobuf authentication encoder');
34
+ let socket; let onMessage = () => {}; let onClose = () => {}; let opened = false; let closed = false; let notified = false; let timer; let rejectOpen;
35
+ const notifyClose = (reason) => { if (!notified) { notified = true; onClose(reason); } };
36
+ const shutdown = (error, code = 'TRANSPORT_CLOSED') => {
37
+ if (closed) return;
38
+ closed = true; opened = false; clearTimeout(timer);
39
+ rejectOpen?.(error || new Error('WSS transport closed'));
40
+ if (socket) { socket.onopen = socket.onmessage = socket.onerror = socket.onclose = null; try { socket.close(); } catch (_) {} }
41
+ notifyClose({ code });
42
+ };
31
43
  return {
32
44
  onMessage: (listener) => { onMessage = listener; },
33
45
  onClose: (listener) => { onClose = listener; },
34
46
  open: () => new Promise((resolve, reject) => {
35
- let timer;
36
- let settled = false;
37
- const finish = (handler, value) => { if (settled) return; settled = true; clearTimeout(timer); handler(value); };
47
+ if (closed || socket) { reject(new Error('WSS transport cannot be reopened')); return; }
48
+ rejectOpen = reject;
38
49
  try {
39
50
  socket = new WebSocketImpl(toWssEndpoint(endpoint, { allowInsecureWs }), protocol);
40
51
  socket.binaryType = 'arraybuffer';
41
- timer = setTimeout(() => { try { socket.close(); } catch (_) {} finish(reject, new Error('WSS connection timed out')); }, openTimeoutMs);
52
+ timer = setTimeout(() => shutdown(new Error('WSS connection timed out'), 'CONNECTION_TIMEOUT'), openTimeoutMs);
42
53
  socket.onopen = () => {
43
- try { socket.send(encodeAuthentication()); }
44
- catch (error) { finish(reject, error); try { socket.close(); } catch (_) {} }
45
- };
46
- socket.onerror = () => finish(reject, new Error('WSS connection failed'));
47
- socket.onclose = (event = {}) => {
48
- if (!opened) finish(reject, new Error(event.reason || 'WSS connection closed before opening'));
49
- onClose({ code: event.code, reason: event.reason });
54
+ if (closed) return;
55
+ try { socket.send(binaryFrame(encodeAuth ? encodeAuth({ token, protocol }) : encode({ type: 'auth', token, protocol }), 'protobuf authentication encoder')); }
56
+ catch (error) { shutdown(error, 'PROTOCOL_ENCODE_ERROR'); }
50
57
  };
58
+ socket.onerror = () => shutdown(new Error('WSS connection failed'), 'CONNECTION_FAILED');
59
+ socket.onclose = (event = {}) => shutdown(new Error('WSS connection closed'), event.code || 'CONNECTION_CLOSED');
51
60
  socket.onmessage = (event) => {
61
+ if (closed) return;
52
62
  try {
63
+ binaryFrame(event.data, 'incoming message');
53
64
  const frame = decode(event.data);
54
- if (authenticationPending) {
65
+ if (!opened) {
55
66
  const rejection = typeof authenticationError === 'function' ? authenticationError(frame) : null;
56
- if (rejection) { finish(reject, new Error(rejection)); try { socket.close(); } catch (_) {} return; }
57
- if (isAuthenticationAccepted(frame)) { authenticationPending = false; opened = true; finish(resolve); onMessage(frame); return; }
67
+ if (rejection) { shutdown(new Error(rejection), 'AUTHENTICATION_REJECTED'); return; }
68
+ if (!isAuthenticationAccepted(frame)) { shutdown(new Error('Unexpected frame before authentication'), 'AUTHENTICATION_REQUIRED'); return; }
69
+ opened = true; clearTimeout(timer); rejectOpen = null; resolve();
70
+ // Core transitions to connected in the open() continuation.
71
+ queueMicrotask(() => { if (!closed) onMessage(frame); });
72
+ return;
58
73
  }
59
74
  onMessage(frame);
60
- } catch (_) { try { socket.close(); } catch (_) {} onClose({ code: 'PROTOCOL_DECODE_ERROR', reason: 'Invalid Protobuf frame' }); }
75
+ } catch (_) { shutdown(new Error('Invalid Protobuf frame'), 'PROTOCOL_DECODE_ERROR'); }
61
76
  };
62
- } catch (error) { finish(reject, error); }
77
+ } catch (error) { shutdown(error, 'CONNECTION_FAILED'); }
63
78
  }),
64
- send: (frame) => {
65
- if (!socket || socket.readyState !== 1) return Promise.reject(new Error('WSS transport is not open'));
66
- try { socket.send(encodeFrame(frame)); return Promise.resolve(); } catch (error) { return Promise.reject(error); }
79
+ send: async (frame) => {
80
+ if (!opened || closed || !socket || socket.readyState !== 1) throw new Error('WSS transport is not authenticated');
81
+ const bytes = binaryFrame(encode(frame), 'protobuf.encode');
82
+ if ((socket.bufferedAmount || 0) + bytes.byteLength > maxBufferedAmount) throw new Error('WSS backpressure limit exceeded');
83
+ socket.send(bytes);
67
84
  },
68
- close: () => new Promise((resolve) => {
69
- if (!socket || socket.readyState === 3) return resolve();
70
- const current = socket.onclose;
71
- socket.onclose = (event) => { if (typeof current === 'function') current(event); resolve(); };
72
- try { socket.close(); } catch (_) { resolve(); }
73
- })
85
+ close: async () => shutdown()
74
86
  };
75
87
  };
76
88
  };
77
89
 
78
- const createChitChatWebClient = ({ endpoint, tokenProvider, protobuf, allowInsecureWs = false, ...options }) => {
90
+ const createChitChatWebClient = ({ endpoint, tokenProvider, protobuf, WebSocketImpl, allowInsecureWs = false, ...options }) => {
79
91
  if (!protobuf || typeof protobuf !== 'object') throw new Error('protobuf codec is required');
80
- return new ReliableRealtimeClient({ endpoint, tokenProvider, runtime: 'web', webTransportFactory: createWssTransportFactory({ ...protobuf, allowInsecureWs }), ...options });
92
+ return new ReliableRealtimeClient({ endpoint, tokenProvider, runtime: 'web', webTransportFactory: createWssTransportFactory({ ...protobuf, WebSocketImpl, allowInsecureWs }), ...options });
93
+ };
94
+
95
+ const createChitChatWebCalls = ({ rtc, ...options } = {}) => {
96
+ const { CallsClient } = requireCore();
97
+ return new CallsClient({ ...options, rtc: rtc || { RTCPeerConnection: globalThis.RTCPeerConnection, MediaStream: globalThis.MediaStream, mediaDevices: globalThis.navigator?.mediaDevices } });
81
98
  };
99
+ const requireCore = () => { try { return require('@chitchat/sdk-core'); } catch (error) { if (error.code !== 'MODULE_NOT_FOUND') throw error; return require(['..', '..', 'chitchat-core', 'src'].join('/')); } };
82
100
 
83
- module.exports = { createChitChatWebClient, createWssTransportFactory, toWssEndpoint };
101
+ module.exports = { createChitChatWebCalls, createChitChatWebClient, createWssTransportFactory, toWssEndpoint };
package/src/react.js ADDED
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+ const React = require('react');
3
+ function useCallState(client) {
4
+ const subscribe = React.useCallback((changed) => client.on('stateChange', changed), [client]);
5
+ const snapshot = React.useCallback(() => client.getSnapshot(), [client]);
6
+ return React.useSyncExternalStore(subscribe, snapshot, snapshot);
7
+ }
8
+ /** Rendering only; acquiring media always requires an explicit user action. */
9
+ const CallVideo = React.forwardRef(function CallVideo({ stream, muted = false, onPlaybackError, ...props }, ref) {
10
+ const element = React.useRef(null);
11
+ React.useImperativeHandle(ref, () => element.current);
12
+ React.useEffect(() => {
13
+ const video = element.current; if (!video) return;
14
+ video.srcObject = stream || null;
15
+ if (stream) video.play().catch(() => onPlaybackError?.({ code: 'PLAYBACK_BLOCKED' }));
16
+ return () => { if (video.srcObject === stream) video.srcObject = null; };
17
+ }, [stream, onPlaybackError]);
18
+ return React.createElement('video', { autoPlay: true, playsInline: true, ...props, muted, ref: element });
19
+ });
20
+ module.exports = { useCallState, CallVideo };