@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.
- package/README.md +299 -0
- 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