@cusuai/web-sdk 0.2.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/LICENSE +21 -0
- package/README.md +338 -0
- package/dist/cusu.iife.js +167 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +8618 -0
- package/package.json +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cusu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
# @cusuai/web-sdk
|
|
2
|
+
|
|
3
|
+
[](https://github.com/cusuai/cusu-web-sdk/actions/workflows/ci.yml)
|
|
4
|
+
[](./docs/testing.md)
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
|
|
7
|
+
Framework-agnostic **customer chat + voice widget** for [Cusu](https://cusuai.com). Call `initialize` once; the messenger mounts into a Shadow DOM host on your page (no iframe CDN). Works with Svelte, React, Vue, or a plain `<script>` tag.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @cusuai/web-sdk
|
|
11
|
+
# or: bun add @cusuai/web-sdk / pnpm add @cusuai/web-sdk / yarn add @cusuai/web-sdk
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## What you get
|
|
17
|
+
|
|
18
|
+
- Floating launcher + chat panel (text, attachments, ratings)
|
|
19
|
+
- Optional **Realtime voice** calls (WebRTC; server-minted session)
|
|
20
|
+
- Dictation via `/v1/transcribe` (Bearer public key)
|
|
21
|
+
- `identify` for logged-in customers (optional HMAC signature from your backend)
|
|
22
|
+
- Locales: English (`en`), Bulgarian (`bg`), Czech (`cs`), Slovak (`sk`), Spanish (`es`), German (`de`), Estonian (`et`), French (`fr`), Polish (`pl`), Hungarian (`hu`), Italian (`it`), Lithuanian (`lt`), Latvian (`lv`), Dutch (`nl`), Norwegian Bokmål (`no`), Portuguese (`pt`), Danish (`da`), Slovenian (`sl`), Croatian (`hr`), Romanian (`ro`), Swedish (`sv`), and Finnish (`fi`); chrome follows the group language from boot
|
|
23
|
+
- Published builds: **ES module** (`dist/index.js`) + **IIFE** (`dist/cusu.iife.js`, global `Cusu`)
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Before you start
|
|
28
|
+
|
|
29
|
+
You need a running Cusu service and a **group** configured for the widget:
|
|
30
|
+
|
|
31
|
+
| Item | Where | Example |
|
|
32
|
+
|------|--------|---------|
|
|
33
|
+
| Group id | Dashboard / install docs | `grp_…` |
|
|
34
|
+
| Public API key | Widget settings (`pk_…`) | Browser-safe; restrict with **allowed origins** |
|
|
35
|
+
| Identify secret (optional) | Group settings (`isk_…`) | **Server only** — never ship to the browser |
|
|
36
|
+
|
|
37
|
+
The SDK talks to the public group surface (`GET/POST /v1/group/…`, `WS /v1/ws/chat`). Wire details: [docs/protocol.md](./docs/protocol.md).
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
import Cusu from '@cusuai/web-sdk';
|
|
45
|
+
|
|
46
|
+
Cusu.onError((error) => {
|
|
47
|
+
console.warn('[cusu]', error.code, error.message);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
Cusu.initialize({
|
|
51
|
+
group: 'grp_…',
|
|
52
|
+
apiKey: 'pk_…',
|
|
53
|
+
// locale: 'en', // optional fallback if boot has no renderable language
|
|
54
|
+
// showLauncher: true // default; set false for a headless launcher
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Optional: attach a logged-in customer (see Identify below)
|
|
58
|
+
Cusu.identify('user_123', {
|
|
59
|
+
name: 'Jane Doe',
|
|
60
|
+
email: 'jane@shop.test',
|
|
61
|
+
gender: 'female'
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
Cusu.open();
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Order that matters**
|
|
68
|
+
|
|
69
|
+
1. Prefer registering `onError` before `initialize`.
|
|
70
|
+
2. Always `initialize` first — the widget boots asynchronously.
|
|
71
|
+
3. `identify` may be called before boot finishes; calls are queued and flushed when ready.
|
|
72
|
+
4. Call `reset()` on host logout so the next visitor (or the next logged-in customer) does not inherit conversations.
|
|
73
|
+
5. Call `destroy()` when tearing down a SPA route that should fully remove the widget.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Install options
|
|
78
|
+
|
|
79
|
+
### ES module (recommended)
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
import Cusu from '@cusuai/web-sdk';
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Types ship with the package (`dist/index.d.ts`).
|
|
86
|
+
|
|
87
|
+
### Script tag (IIFE)
|
|
88
|
+
|
|
89
|
+
After install (or from your CDN of the published `dist/`):
|
|
90
|
+
|
|
91
|
+
```html
|
|
92
|
+
<script src="https://cdn.example.com/cusu.iife.js"></script>
|
|
93
|
+
<script>
|
|
94
|
+
Cusu.initialize({
|
|
95
|
+
group: 'grp_…',
|
|
96
|
+
apiKey: 'pk_…'
|
|
97
|
+
});
|
|
98
|
+
</script>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The IIFE build exposes a global `Cusu` with the same API as the default export.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Configuration
|
|
106
|
+
|
|
107
|
+
`Cusu.initialize(config)` accepts:
|
|
108
|
+
|
|
109
|
+
| Field | Type | Required | Description |
|
|
110
|
+
|-------|------|----------|-------------|
|
|
111
|
+
| `group` | `string` | yes | Stable group id (`grp_…`) from your Cusu install |
|
|
112
|
+
| `apiKey` | `string` | yes | Public API key (`pk_…`) |
|
|
113
|
+
| `apiUrl` | `string` | no | API base URL. Defaults to `https://api.cusuai.com` |
|
|
114
|
+
| `locale` | `'en' \| 'bg' \| 'cs' \| 'sk' \| 'es' \| 'de' \| 'et' \| 'fr' \| 'pl' \| 'hu' \| 'it' \| 'lt' \| 'lv' \| 'nl' \| 'no' \| 'pt' \| 'da' \| 'sl' \| 'hr' \| 'ro' \| 'sv' \| 'fi'` | no | Fallback UI locale when boot language is missing/unsupported |
|
|
115
|
+
| `showLauncher` | `boolean` | no | Floating FAB; default `true` |
|
|
116
|
+
|
|
117
|
+
Calling `initialize` again tears down the previous instance and boots a new one.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Public API
|
|
122
|
+
|
|
123
|
+
| Method | Description |
|
|
124
|
+
|--------|-------------|
|
|
125
|
+
| `initialize(config)` | Mount the widget (async boot) |
|
|
126
|
+
| `identify(id, traits?)` | Link the visitor to your customer id + traits |
|
|
127
|
+
| `open()` / `close()` | Open or close the panel |
|
|
128
|
+
| `isOpened()` | `boolean` — panel open state |
|
|
129
|
+
| `showLauncher()` / `hideLauncher()` | Toggle the floating button at runtime |
|
|
130
|
+
| `reset()` | New anonymous visitor: rotate `cusu_vid`, clear local history, reboot if initialized |
|
|
131
|
+
| `destroy()` | Unmount and clear runtime state |
|
|
132
|
+
| `onError(handler \| null)` | Subscribe to recoverable SDK errors (or clear) |
|
|
133
|
+
|
|
134
|
+
### Headless launcher
|
|
135
|
+
|
|
136
|
+
Hide the FAB and open from your own CTA:
|
|
137
|
+
|
|
138
|
+
```js
|
|
139
|
+
Cusu.initialize({
|
|
140
|
+
group: 'grp_…',
|
|
141
|
+
apiKey: 'pk_…',
|
|
142
|
+
showLauncher: false
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
document.querySelector('#help')?.addEventListener('click', () => Cusu.open());
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Errors
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
Cusu.onError((error) => {
|
|
152
|
+
// error.code — e.g. credits_exhausted
|
|
153
|
+
// error.message — human-readable detail
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Identify (logged-in customers)
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
type IdentifyTraits = {
|
|
163
|
+
name?: string;
|
|
164
|
+
email?: string;
|
|
165
|
+
phone?: string;
|
|
166
|
+
gender?: 'male' | 'female' | 'other';
|
|
167
|
+
signedAt?: number; // unix ms — from your backend when signing
|
|
168
|
+
signature?: string; // hex HMAC-SHA256 — from your backend
|
|
169
|
+
[key: string]: string | number | undefined; // extra traits (not signed)
|
|
170
|
+
};
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
```js
|
|
174
|
+
Cusu.identify('user_123', {
|
|
175
|
+
name: 'Jane Doe',
|
|
176
|
+
email: 'jane@shop.test',
|
|
177
|
+
gender: 'female'
|
|
178
|
+
// + signedAt, signature when the group requires them
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Signed identify (recommended in production)
|
|
183
|
+
|
|
184
|
+
When the group has an identify secret (`isk_…`), unsigned `identify` is **rejected**. Sign on your **backend** only.
|
|
185
|
+
|
|
186
|
+
1. Read (or set) the first-party cookie `cusu_vid` — that value is the `visitorId`.
|
|
187
|
+
2. Build the canonical string (empty optional fields are empty segments):
|
|
188
|
+
|
|
189
|
+
```text
|
|
190
|
+
v1\n{visitorId}\n{externalId}\n{signedAt}\n{name}\n{email}\n{phone}\n{gender}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
3. `signature = hex(HMAC-SHA256(secret, canonical))` with `signedAt = Date.now()` (skew window ±5 minutes).
|
|
194
|
+
4. Return traits + `signedAt` + `signature` to the browser; pass them into `Cusu.identify`.
|
|
195
|
+
|
|
196
|
+
**Node.js example** (Express / any backend):
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
import { createHmac } from 'node:crypto';
|
|
200
|
+
|
|
201
|
+
function signIdentify(secret, { visitorId, externalId, signedAt, name, email, phone, gender }) {
|
|
202
|
+
const canonical = [
|
|
203
|
+
'v1',
|
|
204
|
+
visitorId,
|
|
205
|
+
externalId,
|
|
206
|
+
String(signedAt),
|
|
207
|
+
name ?? '',
|
|
208
|
+
email ?? '',
|
|
209
|
+
phone ?? '',
|
|
210
|
+
gender ?? ''
|
|
211
|
+
].join('\n');
|
|
212
|
+
return createHmac('sha256', secret).update(canonical, 'utf8').digest('hex');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// POST /api/cusu-identify
|
|
216
|
+
app.post('/api/cusu-identify', (req, res) => {
|
|
217
|
+
const externalId = String(req.body.externalId ?? '').trim();
|
|
218
|
+
const traits = req.body.traits ?? {};
|
|
219
|
+
const visitorId = req.cookies.cusu_vid ?? crypto.randomUUID();
|
|
220
|
+
res.cookie('cusu_vid', visitorId, { path: '/', maxAge: 400 * 24 * 60 * 60 * 1000, sameSite: 'lax' });
|
|
221
|
+
|
|
222
|
+
const signedAt = Date.now();
|
|
223
|
+
const signature = signIdentify(process.env.CUSU_IDENTIFY_SECRET, {
|
|
224
|
+
visitorId,
|
|
225
|
+
externalId,
|
|
226
|
+
signedAt,
|
|
227
|
+
name: traits.name,
|
|
228
|
+
email: traits.email,
|
|
229
|
+
phone: traits.phone,
|
|
230
|
+
gender: traits.gender
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
res.json({ ...traits, signedAt, signature });
|
|
234
|
+
});
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Browser:
|
|
238
|
+
|
|
239
|
+
```js
|
|
240
|
+
const identity = await fetch('/api/cusu-identify', {
|
|
241
|
+
method: 'POST',
|
|
242
|
+
headers: { 'content-type': 'application/json' },
|
|
243
|
+
credentials: 'same-origin',
|
|
244
|
+
body: JSON.stringify({
|
|
245
|
+
externalId: 'user_123',
|
|
246
|
+
traits: { name: 'Jane Doe', email: 'jane@shop.test', gender: 'female' }
|
|
247
|
+
})
|
|
248
|
+
}).then((r) => r.json());
|
|
249
|
+
|
|
250
|
+
Cusu.identify('user_123', identity);
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
On **logout**, call `reset()` before the next `identify`. `destroy()` + `initialize()` is not enough: the same `cusu_vid` cookie and local conversation list would keep the previous customer's threads. After `identify`, the SDK replaces local history with that customer's threads from the server.
|
|
254
|
+
|
|
255
|
+
```js
|
|
256
|
+
Cusu.reset();
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Full protocol notes: [docs/protocol.md](./docs/protocol.md). Integrator security checklist: [SECURITY.md](./SECURITY.md).
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Framework notes
|
|
264
|
+
|
|
265
|
+
The SDK is a singleton that mounts into `document`. Use it from any framework the same way — in an effect / `onMount` / `useEffect`, not during SSR.
|
|
266
|
+
|
|
267
|
+
```js
|
|
268
|
+
// React (client component / useEffect)
|
|
269
|
+
useEffect(() => {
|
|
270
|
+
Cusu.initialize({ group, apiKey });
|
|
271
|
+
return () => Cusu.destroy();
|
|
272
|
+
}, [group, apiKey]);
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
```js
|
|
276
|
+
// Svelte
|
|
277
|
+
onMount(() => {
|
|
278
|
+
Cusu.initialize({ group, apiKey });
|
|
279
|
+
return () => Cusu.destroy();
|
|
280
|
+
});
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Do not call `initialize` during server render. Guard with `typeof document !== 'undefined'` if your bundler still evaluates the module on the server.
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Voice & browser requirements
|
|
288
|
+
|
|
289
|
+
- **Chat** needs a modern browser with WebSocket + `fetch`.
|
|
290
|
+
- **Voice calls** need microphone permission and WebRTC (Realtime path only).
|
|
291
|
+
- Prefer **HTTPS** (and **WSS**) in production; public keys should use **allowed origins**.
|
|
292
|
+
- Visitor id lives in cookie `cusu_vid` (`SameSite=Lax`, readable by JS).
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## Development (this repository)
|
|
297
|
+
|
|
298
|
+
Requires [Bun](https://bun.sh) 1.1+.
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
git clone https://github.com/cusuai/cusu-web-sdk.git
|
|
302
|
+
cd cusu-web-sdk
|
|
303
|
+
bun install
|
|
304
|
+
|
|
305
|
+
bun run dev # watch build → dist/
|
|
306
|
+
bun run test # unit tests
|
|
307
|
+
bun run test:coverage # text table + coverage/lcov.info
|
|
308
|
+
bun run test:coverage:check # pure-module ≥ 90% floors
|
|
309
|
+
bun run lint # Biome
|
|
310
|
+
bun run check # svelte-check
|
|
311
|
+
bun run build # production ES + IIFE
|
|
312
|
+
bun run audit # dependency advisories (moderate+)
|
|
313
|
+
bun run ci # full local gate (same as CI except Gitleaks)
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Optional secret scan locally:
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
brew install gitleaks
|
|
320
|
+
gitleaks detect --source . --verbose
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Docs for contributors & auditors
|
|
324
|
+
|
|
325
|
+
| Doc | Topic |
|
|
326
|
+
|-----|--------|
|
|
327
|
+
| [CONTRIBUTING.md](./CONTRIBUTING.md) | Setup, PR checklist, i18n |
|
|
328
|
+
| [docs/testing.md](./docs/testing.md) | Unit tests & coverage policy |
|
|
329
|
+
| [docs/security-ci.md](./docs/security-ci.md) | Audit, Gitleaks, CI artifacts |
|
|
330
|
+
| [docs/protocol.md](./docs/protocol.md) | Backend wire protocol |
|
|
331
|
+
| [SECURITY.md](./SECURITY.md) | Reporting & integrator hardening |
|
|
332
|
+
| [CHANGELOG.md](./CHANGELOG.md) | Release notes |
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## License
|
|
337
|
+
|
|
338
|
+
[MIT](./LICENSE) © Cusu
|