@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 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
+ [![CI](https://github.com/cusuai/cusu-web-sdk/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/cusuai/cusu-web-sdk/actions/workflows/ci.yml)
4
+ [![Coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/cusuai/cusu-web-sdk/main/docs/badges/coverage.json)](./docs/testing.md)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./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