@chatinfra/client 0.0.5 → 0.0.6

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.
Files changed (2) hide show
  1. package/README.md +299 -0
  2. package/package.json +3 -2
package/README.md ADDED
@@ -0,0 +1,299 @@
1
+ # ChatInfra SDK - Installation Guide
2
+
3
+ This guide shows how to install and use the ChatInfra client SDK in:
4
+ - React Native / Expo
5
+ - Web (Browser)
6
+
7
+ > Recommended: **do NOT ship your org secret key in mobile apps**. For production, mint tokens on your backend and return them to the app.
8
+ > If you're prototyping, client-side token minting can work (as shown below).
9
+
10
+ ---
11
+
12
+ ## 1) Install (React Native / Expo)
13
+
14
+ ### A. Install dependencies
15
+
16
+ ```bash
17
+ pnpm add @chatinfra/client
18
+ pnpm add crypto-js
19
+ pnpm add @pusher/pusher-websocket-react-native
20
+ expo install expo-secure-store
21
+ ```
22
+
23
+ ### B. If you're using TypeScript
24
+ ```bash
25
+ pnpm add -D @types/crypto-js
26
+ ```
27
+
28
+ ### C. Create a client wrapper (drop-in style)
29
+
30
+ Create `src/services/chat/ChatInfraClient.ts`:
31
+
32
+ ```ts
33
+ import CryptoJS from "crypto-js";
34
+ import * as SecureStore from "expo-secure-store";
35
+
36
+ import {
37
+ createChatClient,
38
+ PusherReactNativeTransport,
39
+ type Page,
40
+ type Message,
41
+ type Conversation,
42
+ type Identity,
43
+ type TokenStorage,
44
+ } from "@chatinfra/client";
45
+
46
+ export type { Identity, Conversation, Message, Page };
47
+
48
+ type RealtimeCfg = { key: string; cluster: string; force_tls: boolean };
49
+
50
+ type InitTokenPayload = {
51
+ widget_slug: string;
52
+ identity: {
53
+ external_id: string;
54
+ type_key: string;
55
+ fullname?: string | null;
56
+ email?: string | null;
57
+ };
58
+ ttl?: number;
59
+ };
60
+
61
+ type ClientOpts = {
62
+ apiBase: string;
63
+ widgetSlug: string;
64
+
65
+ // optional cached token
66
+ token?: string | null;
67
+
68
+ // client-side minting keys (prototype only)
69
+ orgPublicKey: string;
70
+ orgSecretKey: string;
71
+
72
+ // identity
73
+ identity: InitTokenPayload["identity"];
74
+
75
+ // optional
76
+ ttl?: number;
77
+ realtime?: RealtimeCfg | null;
78
+ tokenStorageKey?: string; // must be SecureStore-safe
79
+ };
80
+
81
+ function trimSlash(s: string) {
82
+ return (s || "").replace(/\/+$/, "");
83
+ }
84
+
85
+ // NOTE: SecureStore keys must be alphanumeric + dot + dash + underscore.
86
+ // Avoid ":" in key names.
87
+ function createExpoTokenStorage(prefix = "chatinfra_"): TokenStorage {
88
+ const keyOf = (k: string) => `${prefix}${k}`;
89
+ return {
90
+ async get(key: string) { return await SecureStore.getItemAsync(keyOf(key)); },
91
+ async set(key: string, value: string) { await SecureStore.setItemAsync(keyOf(key), value); },
92
+ async del(key: string) { await SecureStore.deleteItemAsync(keyOf(key)); },
93
+ };
94
+ }
95
+
96
+ async function safeJson(res: Response) {
97
+ const text = await res.text();
98
+ try { return JSON.parse(text); }
99
+ catch { throw new Error(`Non-JSON response (status ${res.status}): ${text}`); }
100
+ }
101
+
102
+ async function mintTokenWithOrgSecret(params: {
103
+ apiBase: string;
104
+ widgetSlug: string;
105
+ identity: InitTokenPayload["identity"];
106
+ ttl: number;
107
+ orgPublicKey: string;
108
+ orgSecretKey: string;
109
+ }): Promise<string> {
110
+ const payload: InitTokenPayload = {
111
+ widget_slug: params.widgetSlug,
112
+ identity: params.identity,
113
+ ttl: params.ttl,
114
+ };
115
+
116
+ const raw = JSON.stringify(payload);
117
+ const ts = Math.floor(Date.now() / 1000);
118
+ const sig = CryptoJS.HmacSHA256(raw, params.orgSecretKey).toString(CryptoJS.enc.Hex);
119
+
120
+ const res = await fetch(params.apiBase + "/api/v1/widget/init", {
121
+ method: "POST",
122
+ headers: {
123
+ Accept: "application/json",
124
+ "Content-Type": "application/json",
125
+ "X-ORG-KEY": params.orgPublicKey,
126
+ "X-TIMESTAMP": String(ts),
127
+ "X-SIGNATURE": sig,
128
+ },
129
+ body: raw,
130
+ });
131
+
132
+ const json = await safeJson(res);
133
+ if (!json?.ok) throw new Error(json?.message || "Failed to mint token");
134
+
135
+ const t = json?.data?.token || json?.data?.access_token;
136
+ if (!t) throw new Error("Token missing in response");
137
+ return String(t);
138
+ }
139
+
140
+ export class ChatInfraClient {
141
+ private sdk: ReturnType<typeof createChatClient>;
142
+ private me: Identity | null = null;
143
+ private rt: RealtimeCfg | null = null;
144
+
145
+ private storage: TokenStorage;
146
+ private tokenStorageKey: string;
147
+ private apiBase: string;
148
+
149
+ constructor(private opts: ClientOpts) {
150
+ this.apiBase = trimSlash(opts.apiBase);
151
+ this.storage = createExpoTokenStorage();
152
+ this.tokenStorageKey = opts.tokenStorageKey || "chatinfra_token"; // must be SecureStore-safe
153
+
154
+ this.sdk = createChatClient({
155
+ baseUrl: this.apiBase,
156
+ widgetSlug: opts.widgetSlug,
157
+
158
+ identity: {
159
+ externalId: opts.identity.external_id,
160
+ typeKey: opts.identity.type_key,
161
+ fullname: opts.identity.fullname ?? undefined,
162
+ email: opts.identity.email ?? undefined,
163
+ },
164
+
165
+ storage: this.storage,
166
+ tokenStorageKey: this.tokenStorageKey,
167
+
168
+ realtimeTransport: new PusherReactNativeTransport(),
169
+
170
+ tokenProvider: async () => {
171
+ if (opts.token) return String(opts.token);
172
+
173
+ const cached = await this.storage.get(this.tokenStorageKey);
174
+ if (cached) return cached;
175
+
176
+ const token = await mintTokenWithOrgSecret({
177
+ apiBase: this.apiBase,
178
+ widgetSlug: opts.widgetSlug,
179
+ identity: opts.identity,
180
+ ttl: typeof opts.ttl === "number" ? opts.ttl : 3600,
181
+ orgPublicKey: opts.orgPublicKey,
182
+ orgSecretKey: opts.orgSecretKey,
183
+ });
184
+
185
+ await this.storage.set(this.tokenStorageKey, token);
186
+ return token;
187
+ },
188
+ });
189
+
190
+ if (opts.realtime) {
191
+ this.sdk.setRealtimeConfig({
192
+ key: opts.realtime.key,
193
+ cluster: opts.realtime.cluster,
194
+ force_tls: !!opts.realtime.force_tls,
195
+ });
196
+ this.rt = opts.realtime;
197
+ }
198
+ }
199
+
200
+ async init() {
201
+ const info = await this.sdk.init();
202
+ this.me = info.me;
203
+
204
+ this.rt = info.realtime
205
+ ? ({ key: info.realtime.key, cluster: info.realtime.cluster, force_tls: !!info.realtime.force_tls } as any)
206
+ : (this.opts.realtime ?? null);
207
+
208
+ return { me: this.me!, realtime: this.rt, token: info.token };
209
+ }
210
+
211
+ async listConversations(cursor?: string | null) { return await this.sdk.listConversations(cursor ?? null); }
212
+ async listMessages(conversationId: number, cursor?: string | null) { return await this.sdk.listMessages(conversationId, cursor ?? null); }
213
+ async sendMessage(conversationId: number, body: string) { return await this.sdk.sendMessage(conversationId, body); }
214
+ async subscribeConversation(conversationId: number, handlers: any) { return await this.sdk.subscribeConversation(conversationId, handlers); }
215
+ async triggerTyping(conversationId: number, payload: any) { return await this.sdk.triggerTyping(conversationId, payload); }
216
+ async disconnect() { await this.sdk.disconnect(); }
217
+ getMe() { return this.me; }
218
+ }
219
+ ```
220
+
221
+ ### D. Use it in your screen (same constructor shape)
222
+
223
+ ```ts
224
+ const client = new ChatInfraClient({
225
+ apiBase: "https://your-chatinfra-domain.com",
226
+ widgetSlug: "drivon_customer",
227
+ token: cachedToken,
228
+
229
+ orgPublicKey: "pk_...",
230
+ orgSecretKey: "sk_...",
231
+
232
+ identity: {
233
+ external_id: "customer_123",
234
+ type_key: "customer",
235
+ fullname: "Ebuka Mbanusi",
236
+ email: "frankemmanuel249@gmail.com",
237
+ },
238
+ });
239
+ ```
240
+
241
+ ---
242
+
243
+ ## 2) Install (Web)
244
+
245
+ ### A. Install dependencies
246
+
247
+ ```bash
248
+ pnpm add @chatinfra/client
249
+ pnpm add pusher-js
250
+ ```
251
+
252
+ ### B. If you ever see `@react-native-community/netinfo` in the import stack
253
+ That means the **React Native build accidentally imported the web Pusher transport** (which uses `pusher-js/dist/react-native`).
254
+
255
+ Fix:
256
+ - In React Native, only use `PusherReactNativeTransport` and ensure you're importing from the RN entry (or from `@chatinfra/client` if it already exports the RN transport).
257
+ - Remove `@chatinfra/realtime-pusher-web` from your RN dependency graph.
258
+
259
+ ---
260
+
261
+ ## 3) Troubleshooting
262
+
263
+ ### SecureStore: "Invalid key provided..."
264
+ Expo SecureStore only accepts keys containing:
265
+ - letters and numbers
266
+ - `.`, `-`, `_`
267
+
268
+ So these are invalid:
269
+ - `chatinfra:token` (contains `:`)
270
+
271
+ Use safe keys like:
272
+ - `chatinfra_token`
273
+ - `chatinfra.token`
274
+ - `chatinfra-token`
275
+
276
+ In the wrapper above we use a safe prefix `chatinfra_`.
277
+
278
+ ### Conversations not listing but no error
279
+ - Log your network calls:
280
+ - `/api/v1/widget/init` (token mint)
281
+ - `/api/v1/me`
282
+ - `/api/v1/conversations`
283
+ - If `/api/v1/sdk/config` does not exist, pass `realtime` config in the constructor OR add that endpoint in your backend.
284
+
285
+ ---
286
+
287
+ ## 4) Minimal backend endpoints expected by the SDK
288
+
289
+ - `POST /api/v1/widget/init` (if minting in client)
290
+ - `GET /api/v1/me`
291
+ - `GET /api/v1/conversations`
292
+ - `GET /api/v1/conversations/{id}/messages`
293
+ - `POST /api/v1/conversations/{id}/messages`
294
+ - `POST /api/v1/broadcast/auth` (Pusher channel auth)
295
+
296
+ ---
297
+
298
+ ## License / Notes
299
+ For production mobile apps, move token minting to your backend to avoid exposing org secrets.
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@chatinfra/client",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
7
7
  "files": [
8
- "dist"
8
+ "dist",
9
+ "README.md"
9
10
  ],
10
11
  "exports": {
11
12
  ".": {